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/?folder=x [HttpGet] public IEnumerable Get(string folder) { List files = new List(); 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//siteId/folderPath [HttpGet("{siteId}/{path}")] public IEnumerable Get(int siteId, string path) { var folderPath = WebUtility.UrlDecode(path); Folder folder = _folders.GetFolder(siteId, folderPath); List 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//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//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//5 [HttpDelete("{id}")] [Authorize(Roles = Constants.RegisteredRole)] 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 = 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; } } else { _logger.Log(LogLevel.Error, this, LogFunction.Delete, "File Not Found {FileId}", id); HttpContext.Response.StatusCode = 404; } } // GET api//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//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 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//download/5 [HttpGet("download/{id}")] public IActionResult Download(int id) { Models.File file = _files.GetFile(id); if (file != null) { if (_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 {FileId} {FilePath}", id, filepath); 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; } } else { _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Not Found {FileId}", id); HttpContext.Response.StatusCode = 404; 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; } } }