mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat(server): add file expiration feature with automatic cleanup
- Add expiration field to File model in Prisma schema - Create database migration for file expiration - Update File DTOs to support expiration during upload/edit - Create scheduled cleanup script for expired files - Update file controller to handle expiration field in all operations - Update API routes to include expiration in responses - Add npm scripts for running cleanup (dry-run and confirm modes) Co-authored-by: danielalves96 <62755605+danielalves96@users.noreply.github.com>
This commit is contained in:
@@ -27,7 +27,9 @@
|
|||||||
"validate": "pnpm lint && pnpm type-check",
|
"validate": "pnpm lint && pnpm type-check",
|
||||||
"db:seed": "ts-node prisma/seed.js",
|
"db:seed": "ts-node prisma/seed.js",
|
||||||
"cleanup:orphan-files": "tsx src/scripts/cleanup-orphan-files.ts",
|
"cleanup:orphan-files": "tsx src/scripts/cleanup-orphan-files.ts",
|
||||||
"cleanup:orphan-files:confirm": "tsx src/scripts/cleanup-orphan-files.ts --confirm"
|
"cleanup:orphan-files:confirm": "tsx src/scripts/cleanup-orphan-files.ts --confirm",
|
||||||
|
"cleanup:expired-files": "tsx src/scripts/cleanup-expired-files.ts",
|
||||||
|
"cleanup:expired-files:confirm": "tsx src/scripts/cleanup-expired-files.ts --confirm"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "node prisma/seed.js"
|
"seed": "node prisma/seed.js"
|
||||||
|
@@ -0,0 +1,304 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"firstName" TEXT NOT NULL,
|
||||||
|
"lastName" TEXT NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT,
|
||||||
|
"image" TEXT,
|
||||||
|
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"twoFactorEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"twoFactorSecret" TEXT,
|
||||||
|
"twoFactorBackupCodes" TEXT,
|
||||||
|
"twoFactorVerified" BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "files" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"extension" TEXT NOT NULL,
|
||||||
|
"size" BIGINT NOT NULL,
|
||||||
|
"objectName" TEXT NOT NULL,
|
||||||
|
"expiration" DATETIME,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"folderId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "files_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "files_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "folders" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "shares" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT,
|
||||||
|
"views" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"expiration" DATETIME,
|
||||||
|
"description" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"creatorId" TEXT,
|
||||||
|
"securityId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "shares_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "shares_securityId_fkey" FOREIGN KEY ("securityId") REFERENCES "share_security" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "share_security" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"password" TEXT,
|
||||||
|
"maxViews" INTEGER,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "share_recipients" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"shareId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "share_recipients_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "shares" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "app_configs" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"group" TEXT NOT NULL,
|
||||||
|
"isSystem" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "login_attempts" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"attempts" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"lastAttempt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "login_attempts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "password_resets" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expiresAt" DATETIME NOT NULL,
|
||||||
|
"used" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "password_resets_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "share_aliases" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"alias" TEXT NOT NULL,
|
||||||
|
"shareId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "share_aliases_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "shares" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "auth_providers" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"displayName" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"icon" TEXT,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"issuerUrl" TEXT,
|
||||||
|
"clientId" TEXT,
|
||||||
|
"clientSecret" TEXT,
|
||||||
|
"redirectUri" TEXT,
|
||||||
|
"scope" TEXT DEFAULT 'openid profile email',
|
||||||
|
"authorizationEndpoint" TEXT,
|
||||||
|
"tokenEndpoint" TEXT,
|
||||||
|
"userInfoEndpoint" TEXT,
|
||||||
|
"metadata" TEXT,
|
||||||
|
"autoRegister" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"adminEmailDomains" TEXT,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user_auth_providers" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"providerId" TEXT NOT NULL,
|
||||||
|
"provider" TEXT,
|
||||||
|
"externalId" TEXT NOT NULL,
|
||||||
|
"metadata" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "user_auth_providers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "user_auth_providers_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "auth_providers" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reverse_shares" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT,
|
||||||
|
"description" TEXT,
|
||||||
|
"expiration" DATETIME,
|
||||||
|
"maxFiles" INTEGER,
|
||||||
|
"maxFileSize" BIGINT,
|
||||||
|
"allowedFileTypes" TEXT,
|
||||||
|
"password" TEXT,
|
||||||
|
"pageLayout" TEXT NOT NULL DEFAULT 'DEFAULT',
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"nameFieldRequired" TEXT NOT NULL DEFAULT 'OPTIONAL',
|
||||||
|
"emailFieldRequired" TEXT NOT NULL DEFAULT 'OPTIONAL',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"creatorId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "reverse_shares_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reverse_share_files" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"extension" TEXT NOT NULL,
|
||||||
|
"size" BIGINT NOT NULL,
|
||||||
|
"objectName" TEXT NOT NULL,
|
||||||
|
"uploaderEmail" TEXT,
|
||||||
|
"uploaderName" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"reverseShareId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "reverse_share_files_reverseShareId_fkey" FOREIGN KEY ("reverseShareId") REFERENCES "reverse_shares" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reverse_share_aliases" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"alias" TEXT NOT NULL,
|
||||||
|
"reverseShareId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "reverse_share_aliases_reverseShareId_fkey" FOREIGN KEY ("reverseShareId") REFERENCES "reverse_shares" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "trusted_devices" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"deviceHash" TEXT NOT NULL,
|
||||||
|
"deviceName" TEXT,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"lastUsedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"expiresAt" DATETIME NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "trusted_devices_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "folders" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"objectName" TEXT NOT NULL,
|
||||||
|
"parentId" TEXT,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "folders_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "folders" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "folders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ShareFiles" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "_ShareFiles_A_fkey" FOREIGN KEY ("A") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "_ShareFiles_B_fkey" FOREIGN KEY ("B") REFERENCES "shares" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ShareFolders" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "_ShareFolders_A_fkey" FOREIGN KEY ("A") REFERENCES "folders" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "_ShareFolders_B_fkey" FOREIGN KEY ("B") REFERENCES "shares" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "files_folderId_idx" ON "files"("folderId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "shares_securityId_key" ON "shares"("securityId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "app_configs_key_key" ON "app_configs"("key");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "login_attempts_userId_key" ON "login_attempts"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "password_resets_token_key" ON "password_resets"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_aliases_alias_key" ON "share_aliases"("alias");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_aliases_shareId_key" ON "share_aliases"("shareId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "auth_providers_name_key" ON "auth_providers"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_auth_providers_userId_providerId_key" ON "user_auth_providers"("userId", "providerId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_auth_providers_providerId_externalId_key" ON "user_auth_providers"("providerId", "externalId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "reverse_share_aliases_alias_key" ON "reverse_share_aliases"("alias");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "reverse_share_aliases_reverseShareId_key" ON "reverse_share_aliases"("reverseShareId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "trusted_devices_deviceHash_key" ON "trusted_devices"("deviceHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folders_userId_idx" ON "folders"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folders_parentId_idx" ON "folders"("parentId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_ShareFiles_AB_unique" ON "_ShareFiles"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ShareFiles_B_index" ON "_ShareFiles"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_ShareFolders_AB_unique" ON "_ShareFolders"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ShareFolders_B_index" ON "_ShareFolders"("B");
|
3
apps/server/prisma/migrations/migration_lock.toml
Normal file
3
apps/server/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "sqlite"
|
@@ -40,12 +40,13 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
extension String
|
extension String
|
||||||
size BigInt
|
size BigInt
|
||||||
objectName String
|
objectName String
|
||||||
|
expiration DateTime?
|
||||||
|
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
@@ -113,6 +113,7 @@ export class FileController {
|
|||||||
objectName: input.objectName,
|
objectName: input.objectName,
|
||||||
userId,
|
userId,
|
||||||
folderId: input.folderId,
|
folderId: input.folderId,
|
||||||
|
expiration: input.expiration ? new Date(input.expiration) : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ export class FileController {
|
|||||||
objectName: fileRecord.objectName,
|
objectName: fileRecord.objectName,
|
||||||
userId: fileRecord.userId,
|
userId: fileRecord.userId,
|
||||||
folderId: fileRecord.folderId,
|
folderId: fileRecord.folderId,
|
||||||
|
expiration: fileRecord.expiration?.toISOString() || null,
|
||||||
createdAt: fileRecord.createdAt,
|
createdAt: fileRecord.createdAt,
|
||||||
updatedAt: fileRecord.updatedAt,
|
updatedAt: fileRecord.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -429,6 +431,11 @@ export class FileController {
|
|||||||
userId: file.userId,
|
userId: file.userId,
|
||||||
folderId: file.folderId,
|
folderId: file.folderId,
|
||||||
relativePath: file.relativePath || null,
|
relativePath: file.relativePath || null,
|
||||||
|
expiration: file.expiration
|
||||||
|
? file.expiration instanceof Date
|
||||||
|
? file.expiration.toISOString()
|
||||||
|
: file.expiration
|
||||||
|
: null,
|
||||||
createdAt: file.createdAt,
|
createdAt: file.createdAt,
|
||||||
updatedAt: file.updatedAt,
|
updatedAt: file.updatedAt,
|
||||||
}));
|
}));
|
||||||
@@ -502,7 +509,14 @@ export class FileController {
|
|||||||
|
|
||||||
const updatedFile = await prisma.file.update({
|
const updatedFile = await prisma.file.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateData,
|
data: {
|
||||||
|
...updateData,
|
||||||
|
expiration: updateData.expiration
|
||||||
|
? new Date(updateData.expiration)
|
||||||
|
: updateData.expiration === null
|
||||||
|
? null
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileResponse = {
|
const fileResponse = {
|
||||||
@@ -514,6 +528,7 @@ export class FileController {
|
|||||||
objectName: updatedFile.objectName,
|
objectName: updatedFile.objectName,
|
||||||
userId: updatedFile.userId,
|
userId: updatedFile.userId,
|
||||||
folderId: updatedFile.folderId,
|
folderId: updatedFile.folderId,
|
||||||
|
expiration: updatedFile.expiration?.toISOString() || null,
|
||||||
createdAt: updatedFile.createdAt,
|
createdAt: updatedFile.createdAt,
|
||||||
updatedAt: updatedFile.updatedAt,
|
updatedAt: updatedFile.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -571,6 +586,7 @@ export class FileController {
|
|||||||
objectName: updatedFile.objectName,
|
objectName: updatedFile.objectName,
|
||||||
userId: updatedFile.userId,
|
userId: updatedFile.userId,
|
||||||
folderId: updatedFile.folderId,
|
folderId: updatedFile.folderId,
|
||||||
|
expiration: updatedFile.expiration?.toISOString() || null,
|
||||||
createdAt: updatedFile.createdAt,
|
createdAt: updatedFile.createdAt,
|
||||||
updatedAt: updatedFile.updatedAt,
|
updatedAt: updatedFile.updatedAt,
|
||||||
};
|
};
|
||||||
|
@@ -10,6 +10,7 @@ export const RegisterFileSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||||
folderId: z.string().optional(),
|
folderId: z.string().optional(),
|
||||||
|
expiration: z.string().datetime().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CheckFileSchema = z.object({
|
export const CheckFileSchema = z.object({
|
||||||
@@ -22,6 +23,7 @@ export const CheckFileSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||||
folderId: z.string().optional(),
|
folderId: z.string().optional(),
|
||||||
|
expiration: z.string().datetime().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RegisterFileInput = z.infer<typeof RegisterFileSchema>;
|
export type RegisterFileInput = z.infer<typeof RegisterFileSchema>;
|
||||||
@@ -30,6 +32,7 @@ export type CheckFileInput = z.infer<typeof CheckFileSchema>;
|
|||||||
export const UpdateFileSchema = z.object({
|
export const UpdateFileSchema = z.object({
|
||||||
name: z.string().optional().describe("The file name"),
|
name: z.string().optional().describe("The file name"),
|
||||||
description: z.string().optional().nullable().describe("The file description"),
|
description: z.string().optional().nullable().describe("The file description"),
|
||||||
|
expiration: z.string().datetime().optional().nullable().describe("The file expiration date"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MoveFileSchema = z.object({
|
export const MoveFileSchema = z.object({
|
||||||
|
@@ -63,6 +63,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
|||||||
objectName: z.string().describe("The object name of the file"),
|
objectName: z.string().describe("The object name of the file"),
|
||||||
userId: z.string().describe("The user ID"),
|
userId: z.string().describe("The user ID"),
|
||||||
folderId: z.string().nullable().describe("The folder ID"),
|
folderId: z.string().nullable().describe("The folder ID"),
|
||||||
|
expiration: z.string().nullable().describe("The file expiration date"),
|
||||||
createdAt: z.date().describe("The file creation date"),
|
createdAt: z.date().describe("The file creation date"),
|
||||||
updatedAt: z.date().describe("The file last update date"),
|
updatedAt: z.date().describe("The file last update date"),
|
||||||
}),
|
}),
|
||||||
@@ -194,6 +195,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
|||||||
userId: z.string().describe("The user ID"),
|
userId: z.string().describe("The user ID"),
|
||||||
folderId: z.string().nullable().describe("The folder ID"),
|
folderId: z.string().nullable().describe("The folder ID"),
|
||||||
relativePath: z.string().nullable().describe("The relative path (only for recursive listing)"),
|
relativePath: z.string().nullable().describe("The relative path (only for recursive listing)"),
|
||||||
|
expiration: z.string().nullable().describe("The file expiration date"),
|
||||||
createdAt: z.date().describe("The file creation date"),
|
createdAt: z.date().describe("The file creation date"),
|
||||||
updatedAt: z.date().describe("The file last update date"),
|
updatedAt: z.date().describe("The file last update date"),
|
||||||
})
|
})
|
||||||
@@ -230,6 +232,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
|||||||
objectName: z.string().describe("The object name of the file"),
|
objectName: z.string().describe("The object name of the file"),
|
||||||
userId: z.string().describe("The user ID"),
|
userId: z.string().describe("The user ID"),
|
||||||
folderId: z.string().nullable().describe("The folder ID"),
|
folderId: z.string().nullable().describe("The folder ID"),
|
||||||
|
expiration: z.string().nullable().describe("The file expiration date"),
|
||||||
createdAt: z.date().describe("The file creation date"),
|
createdAt: z.date().describe("The file creation date"),
|
||||||
updatedAt: z.date().describe("The file last update date"),
|
updatedAt: z.date().describe("The file last update date"),
|
||||||
}),
|
}),
|
||||||
@@ -269,6 +272,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
|||||||
objectName: z.string().describe("The object name of the file"),
|
objectName: z.string().describe("The object name of the file"),
|
||||||
userId: z.string().describe("The user ID"),
|
userId: z.string().describe("The user ID"),
|
||||||
folderId: z.string().nullable().describe("The folder ID"),
|
folderId: z.string().nullable().describe("The folder ID"),
|
||||||
|
expiration: z.string().nullable().describe("The file expiration date"),
|
||||||
createdAt: z.date().describe("The file creation date"),
|
createdAt: z.date().describe("The file creation date"),
|
||||||
updatedAt: z.date().describe("The file last update date"),
|
updatedAt: z.date().describe("The file last update date"),
|
||||||
}),
|
}),
|
||||||
|
123
apps/server/src/scripts/cleanup-expired-files.ts
Normal file
123
apps/server/src/scripts/cleanup-expired-files.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { isS3Enabled } from "../config/storage.config";
|
||||||
|
import { FilesystemStorageProvider } from "../providers/filesystem-storage.provider";
|
||||||
|
import { S3StorageProvider } from "../providers/s3-storage.provider";
|
||||||
|
import { prisma } from "../shared/prisma";
|
||||||
|
import { StorageProvider } from "../types/storage";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to automatically delete expired files
|
||||||
|
* This script should be run periodically (e.g., via cron job)
|
||||||
|
*/
|
||||||
|
async function cleanupExpiredFiles() {
|
||||||
|
console.log("🧹 Starting expired files cleanup...");
|
||||||
|
console.log(`📦 Storage mode: ${isS3Enabled ? "S3" : "Filesystem"}`);
|
||||||
|
|
||||||
|
let storageProvider: StorageProvider;
|
||||||
|
if (isS3Enabled) {
|
||||||
|
storageProvider = new S3StorageProvider();
|
||||||
|
} else {
|
||||||
|
storageProvider = FilesystemStorageProvider.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all expired files
|
||||||
|
const now = new Date();
|
||||||
|
const expiredFiles = await prisma.file.findMany({
|
||||||
|
where: {
|
||||||
|
expiration: {
|
||||||
|
lte: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
objectName: true,
|
||||||
|
userId: true,
|
||||||
|
size: true,
|
||||||
|
expiration: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📊 Found ${expiredFiles.length} expired files`);
|
||||||
|
|
||||||
|
if (expiredFiles.length === 0) {
|
||||||
|
console.log("\n✨ No expired files found!");
|
||||||
|
return {
|
||||||
|
deletedCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🗑️ Expired files to be deleted:`);
|
||||||
|
expiredFiles.forEach((file) => {
|
||||||
|
const sizeMB = Number(file.size) / (1024 * 1024);
|
||||||
|
console.log(` - ${file.name} (${sizeMB.toFixed(2)} MB) - Expired: ${file.expiration?.toISOString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ask for confirmation (if running interactively)
|
||||||
|
const shouldDelete = process.argv.includes("--confirm");
|
||||||
|
|
||||||
|
if (!shouldDelete) {
|
||||||
|
console.log(`\n⚠️ Dry run mode. To actually delete expired files, run with --confirm flag:`);
|
||||||
|
console.log(` pnpm cleanup:expired-files:confirm`);
|
||||||
|
return {
|
||||||
|
deletedCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
dryRun: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🗑️ Deleting expired files...`);
|
||||||
|
|
||||||
|
let deletedCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
let totalSize = BigInt(0);
|
||||||
|
|
||||||
|
for (const file of expiredFiles) {
|
||||||
|
try {
|
||||||
|
// Delete from storage first
|
||||||
|
await storageProvider.deleteObject(file.objectName);
|
||||||
|
|
||||||
|
// Then delete from database
|
||||||
|
await prisma.file.delete({
|
||||||
|
where: { id: file.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
deletedCount++;
|
||||||
|
totalSize += file.size;
|
||||||
|
console.log(` ✓ Deleted: ${file.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
failedCount++;
|
||||||
|
console.error(` ✗ Failed to delete ${file.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSizeMB = Number(totalSize) / (1024 * 1024);
|
||||||
|
|
||||||
|
console.log(`\n✅ Cleanup complete!`);
|
||||||
|
console.log(` Deleted: ${deletedCount} files (${totalSizeMB.toFixed(2)} MB)`);
|
||||||
|
if (failedCount > 0) {
|
||||||
|
console.log(` Failed: ${failedCount} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedCount,
|
||||||
|
failedCount,
|
||||||
|
totalSize: totalSizeMB,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the cleanup
|
||||||
|
cleanupExpiredFiles()
|
||||||
|
.then((result) => {
|
||||||
|
console.log("\n✨ Script completed successfully");
|
||||||
|
if (result.dryRun) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
process.exit(result.failedCount > 0 ? 1 : 0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("\n❌ Script failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
Reference in New Issue
Block a user