Unindexed files has similar filter to the list file API

This commit is contained in:
2025-11-17 22:20:49 +08:00
parent 82afdb3922
commit 34e78294a1
2 changed files with 63 additions and 52 deletions

View File

@@ -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)) ||

View File

@@ -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;
} }
} }
} }