oqtane.framework/Oqtane.Server/Controllers/FileController.cs
2021-05-27 21:39:43 -04:00

551 lines
20 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;
using Oqtane.Extensions;
// ReSharper disable StringIndexOfIsCultureSpecific.1
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class FileController : Controller
{
private readonly IWebHostEnvironment _environment;
private readonly IFileRepository _files;
private readonly IFolderRepository _folders;
private readonly IUserPermissions _userPermissions;
private readonly ILogManager _logger;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ILogManager logger)
{
_environment = environment;
_files = files;
_folders = folders;
_userPermissions = userPermissions;
_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(RoleNames.Host))
{
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 {SiteId} {Path}", siteId, path);
HttpContext.Response.StatusCode = 404;
return null;
}
return files;
}
// GET api/<controller>/5
[HttpGet("{id}")]
public Models.File Get(int id)
{
Models.File file = _files.GetFile(id);
if (file != null)
{
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;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Not Found {FileId}", id);
HttpContext.Response.StatusCode = 404;
return null;
}
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Roles = RoleNames.Registered)]
public Models.File Put(int id, [FromBody] Models.File file)
{
if (ModelState.IsValid && _userPermissions.IsAuthorized(User, EntityNames.Folder, file.FolderId, PermissionNames.Edit))
{
file.Folder = _folders.GetFolder(file.FolderId);
Models.File _file = _files.GetFile(id, false);
if (_file.Name != file.Name || _file.FolderId != file.FolderId)
{
string folderpath = _folders.GetFolderPath(file.Folder);
if (!Directory.Exists(folderpath))
{
Directory.CreateDirectory(folderpath);
}
System.IO.File.Move(_files.GetFilePath(_file), Path.Combine(folderpath, file.Name));
}
file.Extension = Path.GetExtension(file.Name).ToLower().Replace(".", "");
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 = RoleNames.Registered)]
public void Delete(int id)
{
Models.File file = _files.GetFile(id);
if (file != null)
{
if (_userPermissions.IsAuthorized(User, EntityNames.Folder, file.Folder.FolderId, PermissionNames.Edit))
{
_files.DeleteFile(id);
string filepath = _files.GetFilePath(file);
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;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "File Not Found {FileId}", id);
HttpContext.Response.StatusCode = 404;
}
}
// 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))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"User Not Authorized To Download File {Url} {FolderId}", url, folderid);
HttpContext.Response.StatusCode = 401;
return file;
}
string folderPath = _folders.GetFolderPath(folder);
CreateDirectory(folderPath);
string filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
// check for allowable file extensions
if (!Constants.UploadableFiles.Split(',')
.Contains(Path.GetExtension(filename).ToLower().Replace(".", "")))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
HttpContext.Response.StatusCode = (int) HttpStatusCode.Conflict;
return file;
}
if (!filename.IsPathOrFileValid())
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
$"File Could Not Be Downloaded From Url Due To Its File Name Not Allowed {url}");
HttpContext.Response.StatusCode = (int) HttpStatusCode.Conflict;
return file;
}
try
{
var client = new WebClient();
string targetPath = Path.Combine(folderPath, filename);
// remove file if it already exists
if (System.IO.File.Exists(targetPath))
{
System.IO.File.Delete(targetPath);
}
client.DownloadFile(url, targetPath);
file = _files.AddFile(CreateFile(filename, folder.FolderId, targetPath));
}
catch
{
_logger.Log(LogLevel.Error, this, LogFunction.Create,
"File Could Not Be Downloaded From Url {Url}", url);
}
return file;
}
// POST api/<controller>/upload
[HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile file)
{
if (file.Length <= 0)
{
return;
}
if (!file.FileName.IsPathOrFileValid())
{
HttpContext.Response.StatusCode = (int) HttpStatusCode.Conflict;
return;
}
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 = _folders.GetFolderPath(virtualFolder);
}
}
else
{
if (User.IsInRole(RoleNames.Host))
{
folderPath = GetFolderPath(folder);
}
}
if (!String.IsNullOrEmpty(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, Path.Combine(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 = Path.GetFileNameWithoutExtension(filename); // 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.Split(',').Contains(Path.GetExtension(filename)?.ToLower().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 )
var cleanupFiles = Directory.EnumerateFiles(folder, "*" + token + "*")
.Where(f => Path.GetExtension(f).StartsWith(token));
foreach (var file in cleanupFiles)
{
var createdDate = System.IO.File.GetCreationTime(file).ToUniversalTime();
if (createdDate < DateTime.UtcNow.AddHours(-2))
{
System.IO.File.Delete(file);
}
}
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;
}
/// <summary>
/// Get file with header
/// Content-Disposition: inline
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
/// </summary>
/// <param name="id">File Id from Oqtane filesystem </param>
/// <returns>file content</returns>
// GET api/<controller>/download/5
[HttpGet("download/{id}")]
public IActionResult DownloadInline(int id)
{
return Download(id, false);
}
/// <summary>
/// Get file with header
/// Content-Disposition: attachment; filename="filename.jpg"
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
///
/// </summary>
/// <param name="id">File Id from Oqtane filesystem</param>
/// <returns></returns>
// GET api/<controller>/download/5/attach
[HttpGet("download/{id}/attach")]
public IActionResult DownloadAttachment(int id)
{
return Download(id, true);
}
private IActionResult Download(int id, bool asAttachment)
{
var file = _files.GetFile(id);
if (file != null)
{
if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions))
{
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
{
var result = asAttachment
? PhysicalFile(filepath, file.GetMimeType(), file.Name)
: PhysicalFile(filepath, file.GetMimeType());
return result;
}
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FileId} {FilePath}", id, filepath);
HttpContext.Response.StatusCode = 404;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access File {FileId}", id);
HttpContext.Response.StatusCode = 401;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Not Found {FileId}", id);
HttpContext.Response.StatusCode = 404;
}
string errorPath = Path.Combine(GetFolderPath("images"), "error.png");
return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null;
}
private string GetFolderPath(string folder)
{
return Utilities.PathCombine(_environment.ContentRootPath, folder);
}
private void CreateDirectory(string folderpath)
{
if (!Directory.Exists(folderpath))
{
string path = "";
var separators = new char[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar};
string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries);
foreach (string folder in folders)
{
path = Utilities.PathCombine(path, folder, Path.DirectorySeparatorChar.ToString());
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.Split(',').Contains(file.Extension.ToLower()))
{
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;
}
}
}