SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO IF OBJECT_ID(N'dbo.UserFile', N'U') IS NULL BEGIN CREATE TABLE dbo.UserFile ( UserFileID bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserFile PRIMARY KEY, UserID int NOT NULL, ProjectID int NULL, EntityType nvarchar(100) NOT NULL, EntityID int NOT NULL, FileName nvarchar(300) NOT NULL, OriginalFileName nvarchar(300) NULL, ContentType nvarchar(200) NULL, FileSizeBytes bigint NOT NULL, StoragePath nvarchar(1000) NOT NULL, UploadedDateUTC datetime2 NOT NULL CONSTRAINT DF_UserFile_UploadedDateUTC DEFAULT SYSUTCDATETIME(), DeletedDateUTC datetime2 NULL, IsDeleted bit NOT NULL CONSTRAINT DF_UserFile_IsDeleted DEFAULT 0, CONSTRAINT FK_UserFile_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID), CONSTRAINT FK_UserFile_Project FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID), CONSTRAINT CK_UserFile_FileSizeBytes CHECK (FileSizeBytes >= 0) ); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserFile_User_Active' AND object_id = OBJECT_ID(N'dbo.UserFile')) CREATE INDEX IX_UserFile_User_Active ON dbo.UserFile(UserID, IsDeleted, UploadedDateUTC DESC) INCLUDE (FileSizeBytes, ProjectID, EntityType, EntityID); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserFile_Project_Entity' AND object_id = OBJECT_ID(N'dbo.UserFile')) CREATE INDEX IX_UserFile_Project_Entity ON dbo.UserFile(ProjectID, EntityType, EntityID, IsDeleted); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserFile_StoragePath' AND object_id = OBJECT_ID(N'dbo.UserFile')) CREATE INDEX IX_UserFile_StoragePath ON dbo.UserFile(StoragePath, IsDeleted); GO CREATE OR ALTER PROCEDURE dbo.Project_GetOwnerUserID @ProjectID int AS BEGIN SET NOCOUNT ON; SELECT TOP (1) UserID FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND AccessRole = N'Owner' AND IsActive = 1 ORDER BY ProjectUserAccessID; END; GO CREATE OR ALTER PROCEDURE dbo.UserFile_Create @UserID int, @ProjectID int = NULL, @EntityType nvarchar(100), @EntityID int, @FileName nvarchar(300), @OriginalFileName nvarchar(300) = NULL, @ContentType nvarchar(200) = NULL, @FileSizeBytes bigint, @StoragePath nvarchar(1000) AS BEGIN SET NOCOUNT ON; INSERT dbo.UserFile (UserID, ProjectID, EntityType, EntityID, FileName, OriginalFileName, ContentType, FileSizeBytes, StoragePath) VALUES (@UserID, @ProjectID, @EntityType, @EntityID, @FileName, @OriginalFileName, @ContentType, @FileSizeBytes, @StoragePath); SELECT CAST(SCOPE_IDENTITY() AS bigint); END; GO CREATE OR ALTER PROCEDURE dbo.UserFile_MarkDeletedByStoragePath @StoragePath nvarchar(1000) AS BEGIN SET NOCOUNT ON; UPDATE dbo.UserFile SET IsDeleted = 1, DeletedDateUTC = COALESCE(DeletedDateUTC, SYSUTCDATETIME()) WHERE StoragePath = @StoragePath AND IsDeleted = 0; END; GO CREATE OR ALTER PROCEDURE dbo.UserFile_GetStorageUsageForOwner @UserID int AS BEGIN SET NOCOUNT ON; SELECT CAST(COALESCE(SUM(FileSizeBytes), 0) AS bigint) FROM dbo.UserFile WHERE UserID = @UserID AND IsDeleted = 0; END; GO INSERT INTO dbo.UserFile (UserID, ProjectID, EntityType, EntityID, FileName, OriginalFileName, ContentType, FileSizeBytes, StoragePath, UploadedDateUTC) SELECT ownerAccess.UserID, b.ProjectID, N'SceneAttachment', a.SceneAttachmentID, RIGHT(a.FilePath, CHARINDEX(N'/', REVERSE(a.FilePath + N'/')) - 1), RIGHT(a.FilePath, CHARINDEX(N'/', REVERSE(a.FilePath + N'/')) - 1), NULL, 0, a.FilePath, COALESCE(a.CreatedDateUTC, a.CreatedDate, SYSUTCDATETIME()) FROM dbo.SceneAttachments a INNER JOIN dbo.Scenes s ON s.SceneID = a.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID OUTER APPLY ( SELECT TOP (1) UserID FROM dbo.ProjectUserAccess WHERE ProjectID = b.ProjectID AND AccessRole = N'Owner' AND IsActive = 1 ORDER BY ProjectUserAccessID ) ownerAccess WHERE a.FilePath IS NOT NULL AND a.FilePath LIKE N'/uploads/scenes/%' AND ownerAccess.UserID IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM dbo.UserFile existing WHERE existing.StoragePath = a.FilePath AND existing.EntityType = N'SceneAttachment' AND existing.EntityID = a.SceneAttachmentID ); GO