✨ Unindexed files has similar filter to the list file API
This commit is contained in:
@@ -31,17 +31,17 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path);
|
var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path);
|
||||||
|
|
||||||
// Get all file indexes for this account to extract child folders
|
// Get all file indexes for this account to extract child folders
|
||||||
var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
||||||
|
|
||||||
// Extract unique child folder paths
|
// Extract unique child folder paths
|
||||||
var childFolders = ExtractChildFolders(allFileIndexes, path);
|
var childFolders = ExtractChildFolders(allFileIndexes, path);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Path = path,
|
Path = path,
|
||||||
@@ -76,14 +76,14 @@ public class FileIndexController(
|
|||||||
foreach (var index in allFileIndexes)
|
foreach (var index in allFileIndexes)
|
||||||
{
|
{
|
||||||
var normalizedIndexPath = FileIndexService.NormalizePath(index.Path);
|
var normalizedIndexPath = FileIndexService.NormalizePath(index.Path);
|
||||||
|
|
||||||
// Check if this path is a direct child of the parent path
|
// Check if this path is a direct child of the parent path
|
||||||
if (normalizedIndexPath.StartsWith(normalizedParentPath) &&
|
if (normalizedIndexPath.StartsWith(normalizedParentPath) &&
|
||||||
normalizedIndexPath != normalizedParentPath)
|
normalizedIndexPath != normalizedParentPath)
|
||||||
{
|
{
|
||||||
// Remove the parent path prefix to get the relative path
|
// Remove the parent path prefix to get the relative path
|
||||||
var relativePath = normalizedIndexPath.Substring(normalizedParentPath.Length);
|
var relativePath = normalizedIndexPath.Substring(normalizedParentPath.Length);
|
||||||
|
|
||||||
// Extract the first folder name (direct child)
|
// Extract the first folder name (direct child)
|
||||||
var firstSlashIndex = relativePath.IndexOf('/');
|
var firstSlashIndex = relativePath.IndexOf('/');
|
||||||
if (firstSlashIndex > 0)
|
if (firstSlashIndex > 0)
|
||||||
@@ -108,11 +108,11 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
var fileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Files = fileIndexes,
|
Files = fileIndexes,
|
||||||
@@ -134,11 +134,18 @@ public class FileIndexController(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets files that have not been indexed for the current user.
|
/// Gets files that have not been indexed for the current user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="recycled">Shows recycled files or not</param>
|
||||||
/// <param name="offset">The number of files to skip</param>
|
/// <param name="offset">The number of files to skip</param>
|
||||||
/// <param name="take">The number of files to return</param>
|
/// <param name="take">The number of files to return</param>
|
||||||
|
/// <param name="pool">The pool ID of those files</param>
|
||||||
/// <returns>List of unindexed files</returns>
|
/// <returns>List of unindexed files</returns>
|
||||||
[HttpGet("unindexed")]
|
[HttpGet("unindexed")]
|
||||||
public async Task<IActionResult> GetUnindexedFiles([FromQuery] int offset = 0, [FromQuery] int take = 20)
|
public async Task<IActionResult> GetUnindexedFiles(
|
||||||
|
[FromQuery] Guid? pool,
|
||||||
|
[FromQuery] bool recycled = false,
|
||||||
|
[FromQuery] int offset = 0,
|
||||||
|
[FromQuery] int take = 20
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
@@ -149,14 +156,17 @@ public class FileIndexController(
|
|||||||
{
|
{
|
||||||
var query = db.Files
|
var query = db.Files
|
||||||
.Where(f => f.AccountId == accountId
|
.Where(f => f.AccountId == accountId
|
||||||
&& !f.IsMarkedRecycle
|
&& f.IsMarkedRecycle == recycled
|
||||||
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId))
|
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId))
|
||||||
.OrderByDescending(f => f.CreatedAt);
|
.OrderByDescending(f => f.CreatedAt)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (pool.HasValue) query = query.Where(f => f.PoolId == pool);
|
||||||
|
|
||||||
var totalCount = await query.CountAsync();
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
Response.Headers.Append("X-Total", totalCount.ToString());
|
Response.Headers.Append("X-Total", totalCount.ToString());
|
||||||
|
|
||||||
var unindexedFiles = await query
|
var unindexedFiles = await query
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
@@ -189,7 +199,7 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Verify ownership
|
// Verify ownership
|
||||||
@@ -201,7 +211,7 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||||
|
|
||||||
var updatedIndex = await fileIndexService.UpdateAsync(indexId, request.NewPath);
|
var updatedIndex = await fileIndexService.UpdateAsync(indexId, request.NewPath);
|
||||||
|
|
||||||
if (updatedIndex == null)
|
if (updatedIndex == null)
|
||||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||||
|
|
||||||
@@ -239,7 +249,7 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Verify ownership
|
// Verify ownership
|
||||||
@@ -256,7 +266,7 @@ public class FileIndexController(
|
|||||||
|
|
||||||
// Remove the index
|
// Remove the index
|
||||||
var removed = await fileIndexService.RemoveAsync(indexId);
|
var removed = await fileIndexService.RemoveAsync(indexId);
|
||||||
|
|
||||||
if (!removed)
|
if (!removed)
|
||||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||||
|
|
||||||
@@ -296,7 +306,9 @@ public class FileIndexController(
|
|||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Message = deleteFile ? "File index and file data removed successfully" : "File index removed successfully",
|
Message = deleteFile
|
||||||
|
? "File index and file data removed successfully"
|
||||||
|
: "File index removed successfully",
|
||||||
FileId = fileId,
|
FileId = fileId,
|
||||||
FileName = fileName,
|
FileName = fileName,
|
||||||
Path = filePath,
|
Path = filePath,
|
||||||
@@ -328,7 +340,7 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var removedCount = await fileIndexService.RemoveByPathAsync(accountId, path);
|
var removedCount = await fileIndexService.RemoveByPathAsync(accountId, path);
|
||||||
@@ -357,13 +369,14 @@ public class FileIndexController(
|
|||||||
db.Files.Remove(file);
|
db.Files.Remove(file);
|
||||||
logger.LogInformation("Deleted orphaned file {FileId} after clearing path {Path}", fileId, path);
|
logger.LogInformation("Deleted orphaned file {FileId} after clearing path {Path}", fileId, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Message = deleteFiles ?
|
Message = deleteFiles
|
||||||
$"Cleared {removedCount} file indexes from path and deleted orphaned files" :
|
? $"Cleared {removedCount} file indexes from path and deleted orphaned files"
|
||||||
$"Cleared {removedCount} file indexes from path",
|
: $"Cleared {removedCount} file indexes from path",
|
||||||
Path = path,
|
Path = path,
|
||||||
RemovedCount = removedCount,
|
RemovedCount = removedCount,
|
||||||
FilesDeleted = deleteFiles
|
FilesDeleted = deleteFiles
|
||||||
@@ -393,7 +406,7 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Verify the file exists and belongs to the user
|
// Verify the file exists and belongs to the user
|
||||||
@@ -406,7 +419,8 @@ public class FileIndexController(
|
|||||||
|
|
||||||
// Check if index already exists for this file and path
|
// Check if index already exists for this file and path
|
||||||
var existingIndex = await db.FileIndexes
|
var existingIndex = await db.FileIndexes
|
||||||
.FirstOrDefaultAsync(fi => fi.FileId == request.FileId && fi.Path == request.Path && fi.AccountId == accountId);
|
.FirstOrDefaultAsync(fi =>
|
||||||
|
fi.FileId == request.FileId && fi.Path == request.Path && fi.AccountId == accountId);
|
||||||
|
|
||||||
if (existingIndex != null)
|
if (existingIndex != null)
|
||||||
return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]>
|
return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
@@ -426,7 +440,7 @@ public class FileIndexController(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Failed to create file index for file {FileId} at path {Path} for account {AccountId}",
|
logger.LogError(ex, "Failed to create file index for file {FileId} at path {Path} for account {AccountId}",
|
||||||
request.FileId, request.Path, accountId);
|
request.FileId, request.Path, accountId);
|
||||||
return new ObjectResult(new ApiError
|
return new ObjectResult(new ApiError
|
||||||
{
|
{
|
||||||
@@ -450,7 +464,7 @@ public class FileIndexController(
|
|||||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Build the query with all conditions at once
|
// Build the query with all conditions at once
|
||||||
@@ -458,7 +472,7 @@ public class FileIndexController(
|
|||||||
var fileIndexes = await db.FileIndexes
|
var fileIndexes = await db.FileIndexes
|
||||||
.Where(fi => fi.AccountId == accountId)
|
.Where(fi => fi.AccountId == accountId)
|
||||||
.Include(fi => fi.File)
|
.Include(fi => fi.File)
|
||||||
.Where(fi =>
|
.Where(fi =>
|
||||||
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
|
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
|
||||||
(fi.File.Name.ToLower().Contains(searchTerm) ||
|
(fi.File.Name.ToLower().Contains(searchTerm) ||
|
||||||
(fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) ||
|
(fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) ||
|
||||||
|
|||||||
@@ -63,30 +63,31 @@ public class FileController(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ActionResult> ServeLocalFile(SnCloudFile file)
|
private Task<ActionResult> ServeLocalFile(SnCloudFile file)
|
||||||
{
|
{
|
||||||
// Try temp storage first
|
// Try temp storage first
|
||||||
var tempFilePath = Path.Combine(Path.GetTempPath(), file.Id);
|
var tempFilePath = Path.Combine(Path.GetTempPath(), file.Id);
|
||||||
if (System.IO.File.Exists(tempFilePath))
|
if (System.IO.File.Exists(tempFilePath))
|
||||||
{
|
{
|
||||||
if (file.IsEncrypted)
|
if (file.IsEncrypted)
|
||||||
return StatusCode(StatusCodes.Status403Forbidden, "Encrypted files cannot be accessed before they are processed and stored.");
|
return Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status403Forbidden,
|
||||||
|
"Encrypted files cannot be accessed before they are processed and stored."));
|
||||||
|
|
||||||
return PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream", file.Name, enableRangeProcessing: true);
|
return Task.FromResult<ActionResult>(PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream",
|
||||||
|
file.Name, enableRangeProcessing: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for tus uploads
|
// Fallback for tus uploads
|
||||||
var tusStorePath = configuration.GetValue<string>("Storage:Uploads");
|
var tusStorePath = configuration.GetValue<string>("Storage:Uploads");
|
||||||
if (!string.IsNullOrEmpty(tusStorePath))
|
if (string.IsNullOrEmpty(tusStorePath))
|
||||||
{
|
return Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status400BadRequest,
|
||||||
var tusFilePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id);
|
"File is being processed. Please try again later."));
|
||||||
if (System.IO.File.Exists(tusFilePath))
|
var tusFilePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id);
|
||||||
{
|
return System.IO.File.Exists(tusFilePath)
|
||||||
return PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream", file.Name, enableRangeProcessing: true);
|
? Task.FromResult<ActionResult>(PhysicalFile(tusFilePath, file.MimeType ?? "application/octet-stream",
|
||||||
}
|
file.Name, enableRangeProcessing: true))
|
||||||
}
|
: Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status400BadRequest,
|
||||||
|
"File is being processed. Please try again later."));
|
||||||
return StatusCode(StatusCodes.Status400BadRequest, "File is being processed. Please try again later.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ActionResult> ServeRemoteFile(
|
private async Task<ActionResult> ServeRemoteFile(
|
||||||
@@ -99,7 +100,8 @@ public class FileController(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!file.PoolId.HasValue)
|
if (!file.PoolId.HasValue)
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, "File is in an inconsistent state: uploaded but no pool ID.");
|
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||||
|
"File is in an inconsistent state: uploaded but no pool ID.");
|
||||||
|
|
||||||
var pool = await fs.GetPoolAsync(file.PoolId.Value);
|
var pool = await fs.GetPoolAsync(file.PoolId.Value);
|
||||||
if (pool is null)
|
if (pool is null)
|
||||||
@@ -141,22 +143,17 @@ public class FileController(
|
|||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionResult? TryProxyRedirect(SnCloudFile file, RemoteStorageConfig dest, string fileName)
|
public ActionResult? TryProxyRedirect(SnCloudFile file, RemoteStorageConfig dest, string fileName)
|
||||||
{
|
{
|
||||||
if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false))
|
if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false))
|
||||||
{
|
{
|
||||||
return Redirect(BuildProxyUrl(dest.ImageProxy, fileName));
|
return Redirect(BuildProxyUrl(dest.ImageProxy, fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dest.AccessProxy is not null)
|
return dest.AccessProxy is not null ? Redirect(BuildProxyUrl(dest.AccessProxy, fileName)) : null;
|
||||||
{
|
|
||||||
return Redirect(BuildProxyUrl(dest.AccessProxy, fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildProxyUrl(string proxyUrl, string fileName)
|
private static string BuildProxyUrl(string proxyUrl, string fileName)
|
||||||
{
|
{
|
||||||
var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/");
|
var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/");
|
||||||
var fullUri = new Uri(baseUri, fileName);
|
var fullUri = new Uri(baseUri, fileName);
|
||||||
@@ -189,7 +186,7 @@ public class FileController(
|
|||||||
return Redirect(openUrl);
|
return Redirect(openUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, string> BuildSignedUrlHeaders(
|
private static Dictionary<string, string> BuildSignedUrlHeaders(
|
||||||
SnCloudFile file,
|
SnCloudFile file,
|
||||||
string? fileExtension,
|
string? fileExtension,
|
||||||
string? overrideMimeType,
|
string? overrideMimeType,
|
||||||
@@ -451,4 +448,4 @@ public class FileController(
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user