This repository has been archived on 2025-05-14. You can view files and clone it, but cannot push or open issues or pull requests.
Shaun Walker 02fde9cec3
rolled back change creating an Infrastructure.Interfaces namespace, modified IModule interface to be strongly typed (#343)
* upgrade to .NET Core 3.2 Preview 3 and fixes for issues created by #314

* Components based on Bootstrap4 for Sections and  TabStrip to increase productivity and promote uniformity in Module UIs

* rolled back change creating an Infrastructure.Interfaces namespace, modified IModule interface to be strongly typed
2020-04-05 14:39:08 -04:00

460 lines
17 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Shared;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Oqtane.Security;
using System.Linq;
using System.Drawing;
using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
// ReSharper disable StringIndexOfIsCultureSpecific.1
namespace Oqtane.Controllers
{
[Route("{site}/api/[controller]")]
public class FileController : Controller
{
private readonly IWebHostEnvironment _environment;
private readonly IFileRepository _files;
private readonly IFolderRepository _folders;
private readonly IUserPermissions _userPermissions;
private readonly ITenantResolver _tenants;
private readonly ILogManager _logger;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ITenantResolver tenants, ILogManager logger)
{
_environment = environment;
_files = files;
_folders = folders;
_userPermissions = userPermissions;
_tenants = tenants;
_logger = logger;
}
// GET: api/<controller>?folder=x
[HttpGet]
public IEnumerable<Models.File> Get(string folder)
{
List<Models.File> files = new List<Models.File>();
int folderid;
if (int.TryParse(folder, out folderid))
{
Folder f = _folders.GetFolder(folderid);
if (f != null && _userPermissions.IsAuthorized(User, PermissionNames.Browse, f.Permissions))
{
files = _files.GetFiles(folderid).ToList();
}
}
else
{
if (User.IsInRole(Constants.HostRole))
{
folder = GetFolderPath(folder);
if (Directory.Exists(folder))
{
foreach (string file in Directory.GetFiles(folder))
{
files.Add(new Models.File {Name = Path.GetFileName(file), Extension = Path.GetExtension(file)?.Replace(".", "")});
}
}
}
}
return files;
}
// GET: api/<controller>/siteId/folderPath
[HttpGet("{siteId}/{path}")]
public IEnumerable<Models.File> Get(int siteId, string path)
{
var folderPath = WebUtility.UrlDecode(path);
Folder folder = _folders.GetFolder(siteId, folderPath);
List<Models.File> files;
if (folder != null)
if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.Permissions))
{
files = _files.GetFiles(folder.FolderId).ToList();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Folder {folder}",
folder);
HttpContext.Response.StatusCode = 401;
return null;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "Folder not found {path}",
path);
HttpContext.Response.StatusCode = 401;
return null;
}
return files;
}
// GET api/<controller>/5
[HttpGet("{id}")]
public Models.File Get(int id)
{
Models.File file = _files.GetFile(id);
if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions))
{
return file;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access File {File}", file);
HttpContext.Response.StatusCode = 401;
return null;
}
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = Constants.RegisteredRole)]
public Models.File Put(int id, [FromBody] Models.File file)
{
if (ModelState.IsValid && _userPermissions.IsAuthorized(User, EntityNames.Folder, file.Folder.FolderId, PermissionNames.Edit))
{
file = _files.UpdateFile(file);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "File Updated {File}", file);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, "User Not Authorized To Update File {File}", file);
HttpContext.Response.StatusCode = 401;
file = null;
}
return file;
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Roles = Constants.RegisteredRole)]
public void Delete(int id)
{
Models.File file = _files.GetFile(id);
if (_userPermissions.IsAuthorized(User, EntityNames.Folder, file.Folder.FolderId, PermissionNames.Edit))
{
_files.DeleteFile(id);
string filepath = Path.Combine(GetFolderPath(file.Folder) + file.Name);
if (System.IO.File.Exists(filepath))
{
System.IO.File.Delete(filepath);
}
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "File Deleted {File}", file);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "User Not Authorized To Delete File {FileId}", id);
HttpContext.Response.StatusCode = 401;
}
}
// GET api/<controller>/upload?url=x&folderid=y
[HttpGet("upload")]
public Models.File UploadFile(string url, string folderid)
{
Models.File file = null;
Folder folder = _folders.GetFolder(int.Parse(folderid));
if (folder != null && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.Permissions))
{
string folderPath = GetFolderPath(folder);
CreateDirectory(folderPath);
string filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
// check for allowable file extensions
if (Constants.UploadableFiles.Contains(Path.GetExtension(filename).Replace(".", "")))
{
try
{
var client = new WebClient();
// remove file if it already exists
if (System.IO.File.Exists(folderPath + filename))
{
System.IO.File.Delete(folderPath + filename);
}
client.DownloadFile(url, folderPath + filename);
_files.AddFile(CreateFile(filename, folder.FolderId, folderPath + filename));
}
catch
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url {Url}", url);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Download File {Url} {FolderId}", url, folderid);
HttpContext.Response.StatusCode = 401;
}
return file;
}
// POST api/<controller>/upload
[HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile file)
{
if (file.Length > 0)
{
string folderPath = "";
if (int.TryParse(folder, out int folderId))
{
Folder virtualFolder = _folders.GetFolder(folderId);
if (virtualFolder != null && _userPermissions.IsAuthorized(User, PermissionNames.Edit, virtualFolder.Permissions))
{
folderPath = GetFolderPath(virtualFolder);
}
}
else
{
if (User.IsInRole(Constants.HostRole))
{
folderPath = GetFolderPath(folder);
}
}
if (folderPath != "")
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, file.FileName), FileMode.Create))
{
await file.CopyToAsync(stream);
}
string upload = await MergeFile(folderPath, file.FileName);
if (upload != "" && folderId != -1)
{
_files.AddFile(CreateFile(upload, folderId, folderPath + upload));
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Upload File {Folder} {File}", folder, file);
HttpContext.Response.StatusCode = 401;
}
}
}
private async Task<string> MergeFile(string folder, string filename)
{
string merged = "";
// parse the filename which is in the format of filename.ext.part_x_y
string token = ".part_";
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "x_y"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = filename?.Substring(0, filename.IndexOf(token)); // base filename
string[] fileParts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist ( note that file parts can arrive out of order )
if (fileParts.Length == totalparts && CanAccessFiles(fileParts))
{
// merge file parts
bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{
foreach (string filepart in fileParts)
{
try
{
using (FileStream chunk = new FileStream(filepart, FileMode.Open))
{
await chunk.CopyToAsync(stream);
}
}
catch
{
success = false;
}
}
}
// delete file parts and rename file
if (success)
{
foreach (string filepart in fileParts)
{
System.IO.File.Delete(filepart);
}
// check for allowable file extensions
if (!Constants.UploadableFiles.Contains(Path.GetExtension(filename)?.Replace(".", "")))
{
System.IO.File.Delete(Path.Combine(folder, filename + ".tmp"));
}
else
{
// remove file if it already exists
if (System.IO.File.Exists(Path.Combine(folder, filename)))
{
System.IO.File.Delete(Path.Combine(folder, filename));
}
// rename file now that the entire process is completed
System.IO.File.Move(Path.Combine(folder, filename + ".tmp"), Path.Combine(folder, filename));
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folder, filename));
}
merged = filename;
}
}
// clean up file parts which are more than 2 hours old ( which can happen if a prior file upload failed )
fileParts = Directory.GetFiles(folder, "*" + token + "*");
foreach (string filepart in fileParts)
{
DateTime createddate = System.IO.File.GetCreationTime(filepart).ToUniversalTime();
if (createddate < DateTime.UtcNow.AddHours(-2))
{
System.IO.File.Delete(filepart);
}
}
return merged;
}
private bool CanAccessFiles(string[] files)
{
// ensure files are not locked by another process ( ie. still being written to )
bool canaccess = true;
FileStream stream = null;
foreach (string file in files)
{
int attempts = 0;
bool locked = true;
while (attempts < 5 && locked)
{
try
{
stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.None);
locked = false;
}
catch // file is locked by another process
{
Thread.Sleep(1000); // wait 1 second
}
finally
{
if (stream != null)
{
stream.Close();
}
}
attempts += 1;
}
if (locked && canaccess)
{
canaccess = false;
}
}
return canaccess;
}
// GET api/<controller>/download/5
[HttpGet("download/{id}")]
public IActionResult Download(int id)
{
Models.File file = _files.GetFile(id);
if (file != null && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions))
{
string filepath = GetFolderPath(file.Folder) + file.Name;
if (System.IO.File.Exists(filepath))
{
byte[] filebytes = System.IO.File.ReadAllBytes(filepath);
return File(filebytes, "application/octet-stream", file.Name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {File}", file);
HttpContext.Response.StatusCode = 404;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access File {FileId}", id);
HttpContext.Response.StatusCode = 401;
return null;
}
}
private string GetFolderPath(Folder folder)
{
return _environment.ContentRootPath + "\\Content\\Tenants\\" + _tenants.GetTenant().TenantId.ToString() + "\\Sites\\" + folder.SiteId.ToString() + "\\" + folder.Path;
}
private string GetFolderPath(string folder)
{
return Path.Combine(_environment.WebRootPath, folder);
}
private void CreateDirectory(string folderpath)
{
if (!Directory.Exists(folderpath))
{
string path = "";
string[] folders = folderpath.Split(new[] {'\\'}, StringSplitOptions.RemoveEmptyEntries);
foreach (string folder in folders)
{
path += folder + "\\";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
}
}
private Models.File CreateFile(string filename, int folderid, string filepath)
{
Models.File file = new Models.File();
file.Name = filename;
file.FolderId = folderid;
FileInfo fileinfo = new FileInfo(filepath);
file.Extension = fileinfo.Extension.ToLower().Replace(".", "");
file.Size = (int) fileinfo.Length;
file.ImageHeight = 0;
file.ImageWidth = 0;
if (Constants.ImageFiles.Contains(file.Extension))
{
FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
using (var image = Image.FromStream(stream))
{
file.ImageHeight = image.Height;
file.ImageWidth = image.Width;
}
stream.Close();
}
return file;
}
}
}