mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add support for KV storage mode and enhance attachment handling
This commit is contained in:
@@ -2,6 +2,7 @@ import { Env, User, Invite } from '../types';
|
||||
import { StorageService } from '../services/storage';
|
||||
import { jsonResponse, errorResponse } from '../utils/response';
|
||||
import { generateUUID } from '../utils/uuid';
|
||||
import { deleteBlobObject, getAttachmentObjectKey, getSendFileObjectKey } from '../services/blob-store';
|
||||
|
||||
function isAdmin(user: User): boolean {
|
||||
return user.role === 'admin' && user.status === 'active';
|
||||
@@ -260,7 +261,7 @@ export async function handleAdminDeleteUser(
|
||||
const attachmentMap = await storage.getAttachmentsByUserId(target.id);
|
||||
for (const [cipherId, attachments] of attachmentMap) {
|
||||
for (const att of attachments) {
|
||||
await env.ATTACHMENTS.delete(`${cipherId}/${att.id}`);
|
||||
await deleteBlobObject(env, getAttachmentObjectKey(cipherId, att.id));
|
||||
}
|
||||
}
|
||||
// 2. Send files (keyed by sends/sendId/fileId)
|
||||
@@ -271,7 +272,7 @@ export async function handleAdminDeleteUser(
|
||||
const parsed = JSON.parse(send.data) as Record<string, unknown>;
|
||||
const fileId = typeof parsed.id === 'string' ? parsed.id : null;
|
||||
if (fileId) {
|
||||
await env.ATTACHMENTS.delete(`sends/${send.id}/${fileId}`);
|
||||
await deleteBlobObject(env, getSendFileObjectKey(send.id, fileId));
|
||||
}
|
||||
} catch { /* non-file send or bad data, skip */ }
|
||||
}
|
||||
|
||||
+35
-31
@@ -5,6 +5,13 @@ import { generateUUID } from '../utils/uuid';
|
||||
import { createFileDownloadToken, verifyFileDownloadToken } from '../utils/jwt';
|
||||
import { cipherToResponse } from './ciphers';
|
||||
import { LIMITS } from '../config/limits';
|
||||
import {
|
||||
deleteBlobObject,
|
||||
getAttachmentObjectKey,
|
||||
getBlobObject,
|
||||
getBlobStorageMaxBytes,
|
||||
putBlobObject,
|
||||
} from '../services/blob-store';
|
||||
|
||||
// Format file size to human readable
|
||||
function formatSize(bytes: number): string {
|
||||
@@ -14,11 +21,6 @@ function formatSize(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
// Get R2 object path for attachment
|
||||
function getAttachmentPath(cipherId: string, attachmentId: string): string {
|
||||
return `${cipherId}/${attachmentId}`;
|
||||
}
|
||||
|
||||
// POST /api/ciphers/{cipherId}/attachment/v2
|
||||
// Creates attachment metadata and returns upload URL
|
||||
export async function handleCreateAttachment(
|
||||
@@ -86,9 +88,6 @@ export async function handleCreateAttachment(
|
||||
});
|
||||
}
|
||||
|
||||
// Maximum file size: 100MB
|
||||
const MAX_FILE_SIZE = LIMITS.attachment.maxFileSizeBytes;
|
||||
|
||||
// POST /api/ciphers/{cipherId}/attachment/{attachmentId}
|
||||
// Upload attachment file content
|
||||
export async function handleUploadAttachment(
|
||||
@@ -99,6 +98,7 @@ export async function handleUploadAttachment(
|
||||
attachmentId: string
|
||||
): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const maxFileSize = getBlobStorageMaxBytes(env, LIMITS.attachment.maxFileSizeBytes);
|
||||
|
||||
// Verify cipher exists and belongs to user
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
@@ -114,8 +114,8 @@ export async function handleUploadAttachment(
|
||||
|
||||
// Check content-length header for size limit
|
||||
const contentLength = request.headers.get('content-length');
|
||||
if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) {
|
||||
return errorResponse('File too large. Maximum size is 100MB', 413);
|
||||
if (contentLength && parseInt(contentLength) > maxFileSize) {
|
||||
return errorResponse(`File too large. Maximum size is ${Math.floor(maxFileSize / (1024 * 1024))}MB`, 413);
|
||||
}
|
||||
|
||||
// Get the file from multipart form data
|
||||
@@ -132,21 +132,27 @@ export async function handleUploadAttachment(
|
||||
}
|
||||
|
||||
// Check actual file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return errorResponse('File too large. Maximum size is 100MB', 413);
|
||||
if (file.size > maxFileSize) {
|
||||
return errorResponse(`File too large. Maximum size is ${Math.floor(maxFileSize / (1024 * 1024))}MB`, 413);
|
||||
}
|
||||
|
||||
// Store file in R2
|
||||
const path = getAttachmentPath(cipherId, attachmentId);
|
||||
await env.ATTACHMENTS.put(path, file.stream(), {
|
||||
httpMetadata: {
|
||||
const path = getAttachmentObjectKey(cipherId, attachmentId);
|
||||
try {
|
||||
await putBlobObject(env, path, file.stream(), {
|
||||
size: file.size,
|
||||
contentType: 'application/octet-stream',
|
||||
},
|
||||
customMetadata: {
|
||||
cipherId: cipherId,
|
||||
attachmentId: attachmentId,
|
||||
},
|
||||
});
|
||||
customMetadata: {
|
||||
cipherId,
|
||||
attachmentId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes('KV object too large')) {
|
||||
return errorResponse(`File too large. Maximum size is ${Math.floor(maxFileSize / (1024 * 1024))}MB`, 413);
|
||||
}
|
||||
return errorResponse('Attachment storage is not configured', 500);
|
||||
}
|
||||
|
||||
// Update attachment size if different
|
||||
const actualSize = file.size;
|
||||
@@ -242,9 +248,8 @@ export async function handlePublicDownloadAttachment(
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
|
||||
// Get file from R2
|
||||
const path = getAttachmentPath(cipherId, attachmentId);
|
||||
const object = await env.ATTACHMENTS.get(path);
|
||||
const path = getAttachmentObjectKey(cipherId, attachmentId);
|
||||
const object = await getBlobObject(env, path);
|
||||
|
||||
if (!object) {
|
||||
return errorResponse('Attachment file not found', 404);
|
||||
@@ -257,7 +262,7 @@ export async function handlePublicDownloadAttachment(
|
||||
|
||||
return new Response(object.body, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Type': object.contentType || 'application/octet-stream',
|
||||
'Content-Length': String(object.size),
|
||||
'Cache-Control': 'private, no-cache',
|
||||
},
|
||||
@@ -287,9 +292,8 @@ export async function handleDeleteAttachment(
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
|
||||
// Delete file from R2
|
||||
const path = getAttachmentPath(cipherId, attachmentId);
|
||||
await env.ATTACHMENTS.delete(path);
|
||||
const path = getAttachmentObjectKey(cipherId, attachmentId);
|
||||
await deleteBlobObject(env, path);
|
||||
|
||||
// Delete attachment metadata
|
||||
await storage.deleteAttachment(attachmentId);
|
||||
@@ -318,8 +322,8 @@ export async function deleteAllAttachmentsForCipher(
|
||||
const attachments = await storage.getAttachmentsByCipher(cipherId);
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const path = getAttachmentPath(cipherId, attachment.id);
|
||||
await env.ATTACHMENTS.delete(path);
|
||||
const path = getAttachmentObjectKey(cipherId, attachment.id);
|
||||
await deleteBlobObject(env, path);
|
||||
await storage.deleteAttachment(attachment.id);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-17
@@ -11,6 +11,13 @@ import {
|
||||
verifySendAccessToken,
|
||||
verifySendFileDownloadToken,
|
||||
} from '../utils/jwt';
|
||||
import {
|
||||
deleteBlobObject,
|
||||
getBlobObject,
|
||||
getBlobStorageMaxBytes,
|
||||
getSendFileObjectKey,
|
||||
putBlobObject,
|
||||
} from '../services/blob-store';
|
||||
|
||||
const SEND_INACCESSIBLE_MSG = 'Send does not exist or is no longer available';
|
||||
const SEND_PASSWORD_ITERATIONS = 100_000;
|
||||
@@ -142,10 +149,6 @@ function normalizeSendDataSizeField(data: Record<string, unknown>): Record<strin
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getSendFilePath(sendId: string, fileId: string): string {
|
||||
return `sends/${sendId}/${fileId}`;
|
||||
}
|
||||
|
||||
export function isSendAvailable(send: Send): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
@@ -609,6 +612,7 @@ export async function handleCreateSend(request: Request, env: Env, userId: strin
|
||||
// POST /api/sends/file/v2
|
||||
export async function handleCreateFileSendV2(request: Request, env: Env, userId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const maxFileSize = getBlobStorageMaxBytes(env, LIMITS.send.maxFileSizeBytes);
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
@@ -626,7 +630,7 @@ export async function handleCreateFileSendV2(request: Request, env: Env, userId:
|
||||
const fileLengthRaw = getAliasedProp(body, ['fileLength', 'FileLength']);
|
||||
const fileLengthParsed = parseFileLength(fileLengthRaw.value);
|
||||
if (!fileLengthParsed.ok) return fileLengthParsed.response;
|
||||
if (fileLengthParsed.value > LIMITS.send.maxFileSizeBytes) {
|
||||
if (fileLengthParsed.value > maxFileSize) {
|
||||
return errorResponse('Send storage limit exceeded with this file', 400);
|
||||
}
|
||||
|
||||
@@ -774,6 +778,7 @@ export async function handleUploadSendFile(
|
||||
fileId: string
|
||||
): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const maxFileSize = getBlobStorageMaxBytes(env, LIMITS.send.maxFileSizeBytes);
|
||||
const send = await storage.getSend(sendId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found. Unable to save the file.', 404);
|
||||
@@ -799,7 +804,7 @@ export async function handleUploadSendFile(
|
||||
return errorResponse('No file uploaded', 400);
|
||||
}
|
||||
|
||||
if (file.size > LIMITS.send.maxFileSizeBytes) {
|
||||
if (file.size > maxFileSize) {
|
||||
return errorResponse('Send storage limit exceeded with this file', 413);
|
||||
}
|
||||
|
||||
@@ -813,15 +818,22 @@ export async function handleUploadSendFile(
|
||||
return errorResponse('Send file size does not match.', 400);
|
||||
}
|
||||
|
||||
await env.ATTACHMENTS.put(getSendFilePath(sendId, fileId), file.stream(), {
|
||||
httpMetadata: {
|
||||
try {
|
||||
await putBlobObject(env, getSendFileObjectKey(sendId, fileId), file.stream(), {
|
||||
size: file.size,
|
||||
contentType: 'application/octet-stream',
|
||||
},
|
||||
customMetadata: {
|
||||
sendId,
|
||||
fileId,
|
||||
},
|
||||
});
|
||||
customMetadata: {
|
||||
sendId,
|
||||
fileId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes('KV object too large')) {
|
||||
return errorResponse('Send storage limit exceeded with this file', 413);
|
||||
}
|
||||
return errorResponse('Attachment storage is not configured', 500);
|
||||
}
|
||||
|
||||
await storage.updateRevisionDate(userId);
|
||||
|
||||
@@ -987,7 +999,7 @@ export async function handleDeleteSend(request: Request, env: Env, userId: strin
|
||||
const data = parseStoredSendData(send);
|
||||
const fileId = typeof data.id === 'string' ? data.id : null;
|
||||
if (fileId) {
|
||||
await env.ATTACHMENTS.delete(getSendFilePath(send.id, fileId));
|
||||
await deleteBlobObject(env, getSendFileObjectKey(send.id, fileId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1282,7 +1294,7 @@ export async function handleDownloadSendFile(
|
||||
}
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
const object = await env.ATTACHMENTS.get(getSendFilePath(sendId, fileId));
|
||||
const object = await getBlobObject(env, getSendFileObjectKey(sendId, fileId));
|
||||
if (!object) {
|
||||
return errorResponse('Send file not found', 404);
|
||||
}
|
||||
@@ -1296,7 +1308,7 @@ export async function handleDownloadSendFile(
|
||||
|
||||
return new Response(object.body, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Type': object.contentType || 'application/octet-stream',
|
||||
'Content-Length': String(object.size),
|
||||
'Cache-Control': 'private, no-cache',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user