🗑️ Clean up cloud files

This commit is contained in:
2026-01-11 23:24:49 +08:00
parent b03e9bea5e
commit de1175bdc7
7 changed files with 814 additions and 84 deletions

View File

@@ -0,0 +1,658 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using DysonNetwork.Drive;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DysonNetwork.Drive.Migrations
{
[DbContext(typeof(AppDatabase))]
[Migration("20260111152243_CleanCloudFile")]
partial class CleanCloudFile
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text")
.HasColumnName("description");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<long>("Quota")
.HasColumnType("bigint")
.HasColumnName("quota");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_quota_records");
b.ToTable("quota_records", (string)null);
});
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant?>("CompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("completed_at");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("description");
b.Property<string>("ErrorMessage")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("error_message");
b.Property<long?>("EstimatedDurationSeconds")
.HasColumnType("bigint")
.HasColumnName("estimated_duration_seconds");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<Instant>("LastActivity")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_activity");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("name");
b.Property<Dictionary<string, object>>("Parameters")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("parameters");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<double>("Progress")
.HasColumnType("double precision")
.HasColumnName("progress");
b.Property<Dictionary<string, object>>("Results")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("results");
b.Property<Instant?>("StartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<string>("TaskId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("task_id");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_tasks");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid?>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<BillingConfig>("BillingConfig")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("billing_config");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("description");
b.Property<bool>("IsHidden")
.HasColumnType("boolean")
.HasColumnName("is_hidden");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<PolicyConfig>("PolicyConfig")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("policy_config");
b.Property<RemoteStorageConfig>("StorageConfig")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("storage_config");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_pools");
b.ToTable("pools", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
{
b.Property<string>("Id")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Guid?>("BundleId")
.HasColumnType("uuid")
.HasColumnName("bundle_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<bool>("IsMarkedRecycle")
.HasColumnType("boolean")
.HasColumnName("is_marked_recycle");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<string>("ObjectId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("object_id");
b.Property<Guid?>("PoolId")
.HasColumnType("uuid")
.HasColumnName("pool_id");
b.PrimitiveCollection<string>("SensitiveMarks")
.HasColumnType("jsonb")
.HasColumnName("sensitive_marks");
b.Property<string>("StorageId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("storage_id");
b.Property<string>("StorageUrl")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("storage_url");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("UploadedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("uploaded_at");
b.Property<Dictionary<string, object>>("UserMeta")
.HasColumnType("jsonb")
.HasColumnName("user_meta");
b.HasKey("Id")
.HasName("pk_files");
b.HasIndex("BundleId")
.HasDatabaseName("ix_files_bundle_id");
b.HasIndex("ObjectId")
.HasDatabaseName("ix_files_object_id");
b.HasIndex("PoolId")
.HasDatabaseName("ix_files_pool_id");
b.ToTable("files", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("FileId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("file_id");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("path");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_file_indexes");
b.HasIndex("FileId")
.HasDatabaseName("ix_file_indexes_file_id");
b.HasIndex("Path", "AccountId")
.HasDatabaseName("ix_file_indexes_path_account_id");
b.ToTable("file_indexes", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("description");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<string>("Passcode")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("passcode");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("slug");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_bundles");
b.HasIndex("Slug")
.IsUnique()
.HasDatabaseName("ix_bundles_slug");
b.ToTable("bundles", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
{
b.Property<string>("Id")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<bool>("HasCompression")
.HasColumnType("boolean")
.HasColumnName("has_compression");
b.Property<bool>("HasThumbnail")
.HasColumnType("boolean")
.HasColumnName("has_thumbnail");
b.Property<string>("Hash")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("hash");
b.Property<Dictionary<string, object>>("Meta")
.HasColumnType("jsonb")
.HasColumnName("meta");
b.Property<string>("MimeType")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("mime_type");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_file_objects");
b.ToTable("file_objects", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFilePermission", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("FileId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("file_id");
b.Property<int>("Permission")
.HasColumnType("integer")
.HasColumnName("permission");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("subject_id");
b.Property<int>("SubjectType")
.HasColumnType("integer")
.HasColumnName("subject_type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_file_permissions");
b.ToTable("file_permissions", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<bool>("IsPrimary")
.HasColumnType("boolean")
.HasColumnName("is_primary");
b.Property<string>("ObjectId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("object_id");
b.Property<Guid?>("PoolId")
.HasColumnType("uuid")
.HasColumnName("pool_id");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<string>("StorageId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("storage_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_file_replicas");
b.HasIndex("ObjectId")
.HasDatabaseName("ix_file_replicas_object_id");
b.HasIndex("PoolId")
.HasDatabaseName("ix_file_replicas_pool_id");
b.ToTable("file_replicas", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
.WithMany("Files")
.HasForeignKey("BundleId")
.HasConstraintName("fk_files_bundles_bundle_id");
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
.WithMany()
.HasForeignKey("ObjectId")
.HasConstraintName("fk_files_file_objects_object_id");
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
.WithMany()
.HasForeignKey("PoolId")
.HasConstraintName("fk_files_pools_pool_id");
b.Navigation("Bundle");
b.Navigation("Object");
b.Navigation("Pool");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
.WithMany("FileIndexes")
.HasForeignKey("FileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_file_indexes_files_file_id");
b.Navigation("File");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileReplica", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnFileObject", "Object")
.WithMany("FileReplicas")
.HasForeignKey("ObjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_file_replicas_file_objects_object_id");
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
.WithMany()
.HasForeignKey("PoolId")
.HasConstraintName("fk_file_replicas_pools_pool_id");
b.Navigation("Object");
b.Navigation("Pool");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
{
b.Navigation("FileIndexes");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
{
b.Navigation("Files");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileObject", b =>
{
b.Navigation("FileReplicas");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Drive.Migrations
{
/// <inheritdoc />
public partial class CleanCloudFile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "file_meta",
table: "files");
migrationBuilder.DropColumn(
name: "has_compression",
table: "files");
migrationBuilder.DropColumn(
name: "has_thumbnail",
table: "files");
migrationBuilder.DropColumn(
name: "hash",
table: "files");
migrationBuilder.DropColumn(
name: "is_encrypted",
table: "files");
migrationBuilder.DropColumn(
name: "mime_type",
table: "files");
migrationBuilder.DropColumn(
name: "size",
table: "files");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, object>>(
name: "file_meta",
table: "files",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "has_compression",
table: "files",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "has_thumbnail",
table: "files",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "hash",
table: "files",
type: "character varying(256)",
maxLength: 256,
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "is_encrypted",
table: "files",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "mime_type",
table: "files",
type: "character varying(256)",
maxLength: 256,
nullable: true);
migrationBuilder.AddColumn<long>(
name: "size",
table: "files",
type: "bigint",
nullable: false,
defaultValue: 0L);
}
}
}

View File

@@ -261,36 +261,10 @@ namespace DysonNetwork.Drive.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expired_at"); .HasColumnName("expired_at");
b.Property<Dictionary<string, object>>("FileMeta")
.HasColumnType("jsonb")
.HasColumnName("file_meta");
b.Property<bool>("HasCompression")
.HasColumnType("boolean")
.HasColumnName("has_compression");
b.Property<bool>("HasThumbnail")
.HasColumnType("boolean")
.HasColumnName("has_thumbnail");
b.Property<string>("Hash")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("hash");
b.Property<bool>("IsEncrypted")
.HasColumnType("boolean")
.HasColumnName("is_encrypted");
b.Property<bool>("IsMarkedRecycle") b.Property<bool>("IsMarkedRecycle")
.HasColumnType("boolean") .HasColumnType("boolean")
.HasColumnName("is_marked_recycle"); .HasColumnName("is_marked_recycle");
b.Property<string>("MimeType")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("mime_type");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(1024) .HasMaxLength(1024)
@@ -310,10 +284,6 @@ namespace DysonNetwork.Drive.Migrations
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("sensitive_marks"); .HasColumnName("sensitive_marks");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
b.Property<string>("StorageId") b.Property<string>("StorageId")
.HasMaxLength(32) .HasMaxLength(32)
.HasColumnType("character varying(32)") .HasColumnType("character varying(32)")

View File

@@ -190,12 +190,8 @@ public class BroadcastEventHandler(
{ {
uploadTask = null; uploadTask = null;
} }
if (fileToUpdate.IsEncrypted) if (!pool.PolicyConfig.NoOptimization)
{
uploads.Add((processingFilePath, string.Empty, contentType, false));
}
else if (!pool.PolicyConfig.NoOptimization)
{ {
var fileExtension = Path.GetExtension(processingFilePath); var fileExtension = Path.GetExtension(processingFilePath);
switch (contentType.Split('/')[0]) switch (contentType.Split('/')[0])

View File

@@ -67,10 +67,6 @@ public class FileController(
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)
return Task.FromResult<ActionResult>(StatusCode(StatusCodes.Status403Forbidden,
"Encrypted files cannot be accessed before they are processed and stored."));
return Task.FromResult<ActionResult>(PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream", return Task.FromResult<ActionResult>(PhysicalFile(tempFilePath, file.MimeType ?? "application/octet-stream",
file.Name, enableRangeProcessing: true)); file.Name, enableRangeProcessing: true));
} }

View File

@@ -110,7 +110,9 @@ public class FileService(
var (managedTempPath, fileSize, finalContentType) = var (managedTempPath, fileSize, finalContentType) =
await PrepareFileAsync(fileId, filePath, fileName, contentType); await PrepareFileAsync(fileId, filePath, fileName, contentType);
var file = CreateFileObject(fileId, fileName, finalContentType, fileSize, finalExpiredAt, bundle, accountId); var fileObject = CreateFileObject(fileId, accountId, finalContentType, fileSize);
var file = CreateCloudFile(fileId, fileName, fileObject, finalExpiredAt, bundle, accountId);
if (!pool.PolicyConfig.NoMetadata) if (!pool.PolicyConfig.NoMetadata)
{ {
@@ -118,11 +120,11 @@ public class FileService(
} }
var (processingPath, isTempFile) = var (processingPath, isTempFile) =
await ProcessEncryptionAsync(fileId, managedTempPath, encryptPassword, pool, file); await ProcessEncryptionAsync(fileId, managedTempPath, encryptPassword, pool, fileObject);
file.Hash = await HashFileAsync(processingPath); fileObject.Hash = await HashFileAsync(processingPath);
await SaveFileToDatabaseAsync(file); await SaveFileToDatabaseAsync(file, fileObject);
await PublishFileUploadedEventAsync(file, pool, processingPath, isTempFile); await PublishFileUploadedEventAsync(file, pool, processingPath, isTempFile);
@@ -182,11 +184,26 @@ public class FileService(
return (managedTempPath, fileSize, finalContentType); return (managedTempPath, fileSize, finalContentType);
} }
private SnCloudFile CreateFileObject( private SnFileObject CreateFileObject(
string fileId,
Guid accountId,
string contentType,
long fileSize
)
{
return new SnFileObject
{
Id = fileId,
AccountId = accountId,
MimeType = contentType,
Size = fileSize,
};
}
private SnCloudFile CreateCloudFile(
string fileId, string fileId,
string fileName, string fileName,
string contentType, SnFileObject fileObject,
long fileSize,
Instant? expiredAt, Instant? expiredAt,
SnFileBundle? bundle, SnFileBundle? bundle,
Guid accountId Guid accountId
@@ -196,8 +213,8 @@ public class FileService(
{ {
Id = fileId, Id = fileId,
Name = fileName, Name = fileName,
MimeType = contentType, Object = fileObject,
Size = fileSize, ObjectId = fileId,
ExpiredAt = expiredAt, ExpiredAt = expiredAt,
BundleId = bundle?.Id, BundleId = bundle?.Id,
AccountId = accountId, AccountId = accountId,
@@ -209,7 +226,7 @@ public class FileService(
string managedTempPath, string managedTempPath,
string? encryptPassword, string? encryptPassword,
FilePool pool, FilePool pool,
SnCloudFile file SnFileObject fileObject
) )
{ {
if (string.IsNullOrWhiteSpace(encryptPassword)) if (string.IsNullOrWhiteSpace(encryptPassword))
@@ -223,27 +240,14 @@ public class FileService(
File.Delete(managedTempPath); File.Delete(managedTempPath);
file.IsEncrypted = true; fileObject.MimeType = "application/octet-stream";
file.MimeType = "application/octet-stream"; fileObject.Size = new FileInfo(encryptedPath).Length;
file.Size = new FileInfo(encryptedPath).Length;
return Task.FromResult((encryptedPath, true)); return Task.FromResult((encryptedPath, true));
} }
private async Task SaveFileToDatabaseAsync(SnCloudFile file) private async Task SaveFileToDatabaseAsync(SnCloudFile file, SnFileObject fileObject)
{ {
var fileObject = new SnFileObject
{
Id = file.Id,
AccountId = file.AccountId,
Size = file.Size,
Meta = file.FileMeta,
MimeType = file.MimeType,
Hash = file.Hash,
HasCompression = file.HasCompression,
HasThumbnail = file.HasThumbnail
};
var replica = new SnFileReplica var replica = new SnFileReplica
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
@@ -254,9 +258,19 @@ public class FileService(
IsPrimary = true IsPrimary = true
}; };
var permission = new SnFilePermission
{
Id = Guid.NewGuid(),
FileId = file.Id,
SubjectType = SnFilePermissionType.Someone,
SubjectId = file.AccountId.ToString(),
Permission = SnFilePermissionLevel.Write
};
db.Files.Add(file); db.Files.Add(file);
db.FileObjects.Add(fileObject); db.FileObjects.Add(fileObject);
db.FileReplicas.Add(replica); db.FileReplicas.Add(replica);
db.FilePermissions.Add(permission);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
file.ObjectId = file.Id; file.ObjectId = file.Id;
@@ -327,11 +341,11 @@ public class FileService(
if (orientation is 6 or 8) (width, height) = (height, width); if (orientation is 6 or 8) (width, height) = (height, width);
meta["exif"] = exif; meta["exif"] = exif;
meta["ratio"] = height != 0 ? (double)width / height : 0; meta["ratio"] = height != 0 ? (double)width / height : 0;
file.FileMeta = meta; file.Object!.Meta = meta;
} }
catch (Exception ex) catch (Exception ex)
{ {
file.FileMeta = new Dictionary<string, object?>(); file.Object!.Meta = new Dictionary<string, object?>();
logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id); logger.LogError(ex, "Failed to analyze image file {FileId}", file.Id);
} }
@@ -342,7 +356,7 @@ public class FileService(
try try
{ {
var mediaInfo = await FFProbe.AnalyseAsync(filePath); var mediaInfo = await FFProbe.AnalyseAsync(filePath);
file.FileMeta = new Dictionary<string, object?> file.Object!.Meta = new Dictionary<string, object?>
{ {
["width"] = mediaInfo.PrimaryVideoStream?.Width, ["width"] = mediaInfo.PrimaryVideoStream?.Width,
["height"] = mediaInfo.PrimaryVideoStream?.Height, ["height"] = mediaInfo.PrimaryVideoStream?.Height,
@@ -378,8 +392,8 @@ public class FileService(
.ToList(), .ToList(),
}; };
if (mediaInfo.PrimaryVideoStream is not null) if (mediaInfo.PrimaryVideoStream is not null)
file.FileMeta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width / file.Object!.Meta["ratio"] = (double)mediaInfo.PrimaryVideoStream.Width /
mediaInfo.PrimaryVideoStream.Height; mediaInfo.PrimaryVideoStream.Height;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -856,7 +870,6 @@ file class UpdatableCloudFile(SnCloudFile file)
return setter => setter return setter => setter
.SetProperty(f => f.Name, Name) .SetProperty(f => f.Name, Name)
.SetProperty(f => f.Description, Description) .SetProperty(f => f.Description, Description)
.SetProperty(f => f.FileMeta, FileMeta)
.SetProperty(f => f.UserMeta, userMeta) .SetProperty(f => f.UserMeta, userMeta)
.SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle); .SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle);
} }

View File

@@ -13,23 +13,25 @@ public class SnCloudFile : ModelBase, ICloudFile, IIdentifiedResource
[MaxLength(32)] [MaxLength(32)]
public string Id { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty); public string Id { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty);
[MaxLength(1024)] public string Name { get; set; } = string.Empty; [MaxLength(1024)] public string Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; } [MaxLength(4096)] public string? Description { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object?>? FileMeta { get; set; }
[Column(TypeName = "jsonb")] public Dictionary<string, object?>? UserMeta { get; set; } [Column(TypeName = "jsonb")] public Dictionary<string, object?>? UserMeta { get; set; }
[Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = [];
[MaxLength(256)] public string? MimeType { get; set; }
[MaxLength(256)] public string? Hash { get; set; } [NotMapped]
[Column(TypeName = "jsonb")]
public Dictionary<string, object?> FileMeta => Object!.Meta ?? [];
[NotMapped] [MaxLength(256)] public string? MimeType => Object!.MimeType;
[NotMapped] [MaxLength(256)] public string? Hash => Object!.Hash;
public Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }
public long Size { get; set; } [NotMapped] public long Size => Object!.Size;
public Instant? UploadedAt { get; set; } public Instant? UploadedAt { get; set; }
public bool HasCompression { get; set; } = false; [NotMapped] public bool HasCompression => Object!.HasCompression;
public bool HasThumbnail { get; set; } = false; [NotMapped] public bool HasThumbnail => Object!.HasThumbnail;
public bool IsEncrypted { get; set; } = false;
[MaxLength(32)] public string? ObjectId { get; set; } [MaxLength(32)] public string? ObjectId { get; set; }
public SnFileObject? Object { get; set; } public SnFileObject? Object { get; set; }
public FilePool? Pool { get; set; } public FilePool? Pool { get; set; }
public Guid? PoolId { get; set; } public Guid? PoolId { get; set; }
[JsonIgnore] public SnFileBundle? Bundle { get; set; } [JsonIgnore] public SnFileBundle? Bundle { get; set; }
@@ -60,7 +62,7 @@ public class SnCloudFile : ModelBase, ICloudFile, IIdentifiedResource
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FastUploadLink { get; set; } public string? FastUploadLink { get; set; }
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
public SnCloudFileReferenceObject ToReferenceObject() public SnCloudFileReferenceObject ToReferenceObject()
{ {
@@ -109,4 +111,4 @@ public class SnCloudFile : ModelBase, ICloudFile, IIdentifiedResource
return proto; return proto;
} }
} }