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",
|
||||
"db:seed": "ts-node prisma/seed.js",
|
||||
"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": {
|
||||
"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 {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
extension String
|
||||
size BigInt
|
||||
objectName String
|
||||
expiration DateTime?
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
@@ -113,6 +113,7 @@ export class FileController {
|
||||
objectName: input.objectName,
|
||||
userId,
|
||||
folderId: input.folderId,
|
||||
expiration: input.expiration ? new Date(input.expiration) : null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -125,6 +126,7 @@ export class FileController {
|
||||
objectName: fileRecord.objectName,
|
||||
userId: fileRecord.userId,
|
||||
folderId: fileRecord.folderId,
|
||||
expiration: fileRecord.expiration?.toISOString() || null,
|
||||
createdAt: fileRecord.createdAt,
|
||||
updatedAt: fileRecord.updatedAt,
|
||||
};
|
||||
@@ -429,6 +431,11 @@ export class FileController {
|
||||
userId: file.userId,
|
||||
folderId: file.folderId,
|
||||
relativePath: file.relativePath || null,
|
||||
expiration: file.expiration
|
||||
? file.expiration instanceof Date
|
||||
? file.expiration.toISOString()
|
||||
: file.expiration
|
||||
: null,
|
||||
createdAt: file.createdAt,
|
||||
updatedAt: file.updatedAt,
|
||||
}));
|
||||
@@ -502,7 +509,14 @@ export class FileController {
|
||||
|
||||
const updatedFile = await prisma.file.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
data: {
|
||||
...updateData,
|
||||
expiration: updateData.expiration
|
||||
? new Date(updateData.expiration)
|
||||
: updateData.expiration === null
|
||||
? null
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const fileResponse = {
|
||||
@@ -514,6 +528,7 @@ export class FileController {
|
||||
objectName: updatedFile.objectName,
|
||||
userId: updatedFile.userId,
|
||||
folderId: updatedFile.folderId,
|
||||
expiration: updatedFile.expiration?.toISOString() || null,
|
||||
createdAt: updatedFile.createdAt,
|
||||
updatedAt: updatedFile.updatedAt,
|
||||
};
|
||||
@@ -571,6 +586,7 @@ export class FileController {
|
||||
objectName: updatedFile.objectName,
|
||||
userId: updatedFile.userId,
|
||||
folderId: updatedFile.folderId,
|
||||
expiration: updatedFile.expiration?.toISOString() || null,
|
||||
createdAt: updatedFile.createdAt,
|
||||
updatedAt: updatedFile.updatedAt,
|
||||
};
|
||||
|
@@ -10,6 +10,7 @@ export const RegisterFileSchema = z.object({
|
||||
}),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
folderId: z.string().optional(),
|
||||
expiration: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
export const CheckFileSchema = z.object({
|
||||
@@ -22,6 +23,7 @@ export const CheckFileSchema = z.object({
|
||||
}),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
folderId: z.string().optional(),
|
||||
expiration: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
export type RegisterFileInput = z.infer<typeof RegisterFileSchema>;
|
||||
@@ -30,6 +32,7 @@ export type CheckFileInput = z.infer<typeof CheckFileSchema>;
|
||||
export const UpdateFileSchema = z.object({
|
||||
name: z.string().optional().describe("The file name"),
|
||||
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({
|
||||
|
@@ -63,6 +63,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
objectName: z.string().describe("The object name of the file"),
|
||||
userId: z.string().describe("The user 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"),
|
||||
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"),
|
||||
folderId: z.string().nullable().describe("The folder ID"),
|
||||
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"),
|
||||
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"),
|
||||
userId: z.string().describe("The user 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"),
|
||||
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"),
|
||||
userId: z.string().describe("The user 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"),
|
||||
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