Merge pull request #5404 from sbwalker/dev

add new option to FileManager component to anonymize filenames during upload
This commit is contained in:
Shaun Walker
2025-07-21 09:14:30 -04:00
committed by GitHub
4 changed files with 34 additions and 23 deletions

View File

@ -157,6 +157,9 @@
[Parameter] [Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public bool AnonymizeUploadFilenames { get; set; } = false; // optional - indicate if file names should be anonymized on upload - default false
[Parameter] [Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
@ -408,7 +411,7 @@
} }
// upload files // upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token); var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, AnonymizeUploadFilenames, tokenSource.Token);
// reset progress indicators // reset progress indicators
if (ShowProgress) if (ShowProgress)

View File

@ -224,17 +224,17 @@ namespace Oqtane.UI
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt) public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
{ {
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1); UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1, false);
return Task.CompletedTask; return Task.CompletedTask;
} }
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default) public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, bool anonymizeuploadfilenames, CancellationToken cancellationToken = default)
{ {
try try
{ {
return _jsRuntime.InvokeAsync<bool>( return _jsRuntime.InvokeAsync<bool>(
"Oqtane.Interop.uploadFiles", cancellationToken, "Oqtane.Interop.uploadFiles", cancellationToken,
posturl, folder, id, antiforgerytoken, jwt, chunksize); posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames);
} }
catch catch
{ {

View File

@ -444,9 +444,14 @@ namespace Oqtane.Controllers
} }
// ensure filename is valid // ensure filename is valid
if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName)) string fileName = formfile.FileName;
if (Path.GetExtension(fileName).Contains(':'))
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); fileName = fileName.Substring(0, fileName.LastIndexOf(':')); // remove invalid suffix from extension
}
if (!fileName.IsPathOrFileValid() || !HasValidFileExtension(fileName))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", fileName);
return StatusCode((int)HttpStatusCode.Forbidden); return StatusCode((int)HttpStatusCode.Forbidden);
} }
@ -458,8 +463,8 @@ namespace Oqtane.Controllers
return StatusCode((int)HttpStatusCode.Forbidden); return StatusCode((int)HttpStatusCode.Forbidden);
} }
// create file name using header values // create file name using header part values
string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); fileName += ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000");
string folderPath = ""; string folderPath = "";
try try
@ -532,13 +537,13 @@ namespace Oqtane.Controllers
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999" string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1)); int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = Path.GetFileNameWithoutExtension(filename); // base filename filename = Path.GetFileNameWithoutExtension(filename); // base filename including original file extension
string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts 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 all of the file parts exist (note that file parts can arrive out of order)
if (fileparts.Length == totalparts && CanAccessFiles(fileparts)) if (fileparts.Length == totalparts && CanAccessFiles(fileparts))
{ {
// merge file parts into temp file (in case another user is trying to get the file) // merge file parts into temp file (in case another user is trying to read the file)
bool success = true; bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{ {
@ -559,9 +564,7 @@ namespace Oqtane.Controllers
} }
// clean up file parts // clean up file parts
foreach (var file in Directory.GetFiles(folder, "*" + token + "*")) foreach (var file in fileparts)
{
if (fileparts.Contains(file))
{ {
try try
{ {
@ -572,12 +575,11 @@ namespace Oqtane.Controllers
// unable to delete part - ignore // unable to delete part - ignore
} }
} }
}
// rename temp file // rename temp file
if (success) if (success)
{ {
// remove file if it already exists (as well as any thumbnails which may exist) // remove existing file (as well as any thumbnails)
foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*")) foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*"))
{ {
if (Path.GetExtension(file) != ".tmp") if (Path.GetExtension(file) != ".tmp")

View File

@ -311,7 +311,7 @@ Oqtane.Interop = {
} }
return files; return files;
}, },
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) {
var success = true; var success = true;
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
@ -344,16 +344,22 @@ Oqtane.Interop = {
const totalParts = Math.ceil(file.size / chunkSize); const totalParts = Math.ceil(file.size / chunkSize);
let partCount = 0; let partCount = 0;
let filename = file.name;
if (anonymizeuploadfilenames) {
filename = crypto.randomUUID() + '.' + filename.split('.').pop();
}
const uploadPart = () => { const uploadPart = () => {
const start = partCount * chunkSize; const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size); const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); const chunk = file.slice(start, end);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let formdata = new FormData(); let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken); formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder); formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name); formdata.append('formfile', chunk, filename);
var credentials = 'same-origin'; var credentials = 'same-origin';
var headers = new Headers(); var headers = new Headers();