fix(send): refresh routes and gate file access

This commit is contained in:
shuaiplus
2026-07-05 23:44:07 +08:00
parent d9a36fefe6
commit 8c481a1564
4 changed files with 31 additions and 12 deletions
+7 -1
View File
@@ -8,6 +8,7 @@ import { LIMITS } from '../config/limits';
import { import {
getBlobStorageMaxBytes, getBlobStorageMaxBytes,
getSendFileObjectKey, getSendFileObjectKey,
getBlobObject,
putBlobObject, putBlobObject,
deleteBlobObject, deleteBlobObject,
} from '../services/blob-store'; } from '../services/blob-store';
@@ -82,8 +83,13 @@ async function processSendFileUpload(
return upload; return upload;
} }
const path = getSendFileObjectKey(send.id, fileId);
if (await getBlobObject(env, path)) {
return errorResponse('Send file has already been uploaded', 409);
}
try { try {
await putBlobObject(env, getSendFileObjectKey(send.id, fileId), upload.body, { await putBlobObject(env, path, upload.body, {
size: upload.size, size: upload.size,
contentType: upload.contentType, contentType: upload.contentType,
customMetadata: { customMetadata: {
+10 -2
View File
@@ -290,12 +290,20 @@ export async function handleDownloadSendFile(
} }
const storage = new StorageService(env.DB); const storage = new StorageService(env.DB);
const send = await storage.getSend(sendId);
if (!send || !isSendAvailable(send) || send.type !== SendType.File) {
return errorResponse(SEND_INACCESSIBLE_MSG, 404);
}
const data = parseStoredSendData(send);
const expectedFileId = typeof data.id === 'string' ? data.id : null;
if (!expectedFileId || expectedFileId !== fileId) {
return errorResponse(SEND_INACCESSIBLE_MSG, 404);
}
const object = await getBlobObject(env, getSendFileObjectKey(sendId, fileId)); const object = await getBlobObject(env, getSendFileObjectKey(sendId, fileId));
if (!object) { if (!object) {
return errorResponse('Send file not found', 404); return errorResponse('Send file not found', 404);
} }
const send = await storage.getSend(sendId);
const data = send ? parseStoredSendData(send) : {};
const fileName = typeof data.fileName === 'string' ? data.fileName : fileId; const fileName = typeof data.fileName === 'string' ? data.fileName : fileId;
const firstUse = await storage.consumeAttachmentDownloadToken(`send:${claims.jti}`, claims.exp); const firstUse = await storage.consumeAttachmentDownloadToken(`send:${claims.jti}`, claims.exp);
+6 -2
View File
@@ -93,9 +93,13 @@ export async function incrementSendAccessCount(db: D1Database, sendId: string):
const result = await db const result = await db
.prepare( .prepare(
'UPDATE sends SET access_count = access_count + 1, updated_at = ? ' + 'UPDATE sends SET access_count = access_count + 1, updated_at = ? ' +
'WHERE id = ? AND (max_access_count IS NULL OR access_count < max_access_count)' 'WHERE id = ? ' +
'AND disabled = 0 ' +
'AND (max_access_count IS NULL OR access_count < max_access_count) ' +
'AND (expiration_date IS NULL OR expiration_date > ?) ' +
'AND deletion_date > ?'
) )
.bind(now, sendId) .bind(now, sendId, now, now)
.run(); .run();
return (result.meta.changes ?? 0) > 0; return (result.meta.changes ?? 0) > 0;
} }
+8 -7
View File
@@ -228,6 +228,7 @@ export default function App() {
hint: null, hint: null,
}); });
const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(initialInviteCode); const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(initialInviteCode);
const [hashPathRaw, setHashPathRaw] = useState(() => (typeof window !== 'undefined' ? window.location.hash || '' : ''));
const [unlockPassword, setUnlockPassword] = useState(''); const [unlockPassword, setUnlockPassword] = useState('');
const [pendingTotp, setPendingTotp] = useState<PendingTotp | null>(null); const [pendingTotp, setPendingTotp] = useState<PendingTotp | null>(null);
const [pendingTotpMode, setPendingTotpMode] = useState<'login' | 'unlock' | null>(null); const [pendingTotpMode, setPendingTotpMode] = useState<'login' | 'unlock' | null>(null);
@@ -295,15 +296,16 @@ export default function App() {
}, [pushToast]); }, [pushToast]);
useEffect(() => { useEffect(() => {
const syncInviteFromUrl = () => { const syncUrlState = () => {
setInviteCodeFromUrl(readInviteCodeFromUrl()); setInviteCodeFromUrl(readInviteCodeFromUrl());
setHashPathRaw(window.location.hash || '');
}; };
syncInviteFromUrl(); syncUrlState();
window.addEventListener('hashchange', syncInviteFromUrl); window.addEventListener('hashchange', syncUrlState);
window.addEventListener('popstate', syncInviteFromUrl); window.addEventListener('popstate', syncUrlState);
return () => { return () => {
window.removeEventListener('hashchange', syncInviteFromUrl); window.removeEventListener('hashchange', syncUrlState);
window.removeEventListener('popstate', syncInviteFromUrl); window.removeEventListener('popstate', syncUrlState);
}; };
}, []); }, []);
@@ -1862,7 +1864,6 @@ export default function App() {
await pendingAuthRequestsQuery.refetch(); await pendingAuthRequestsQuery.refetch();
}; };
const hashPathRaw = typeof window !== 'undefined' ? window.location.hash || '' : '';
const hashPath = hashPathRaw.startsWith('#') ? hashPathRaw.slice(1) : hashPathRaw; const hashPath = hashPathRaw.startsWith('#') ? hashPathRaw.slice(1) : hashPathRaw;
const hashPathOnly = String(hashPath || '').split('?')[0].split('#')[0]; const hashPathOnly = String(hashPath || '').split('?')[0].split('#')[0];
const trimmedHashPath = hashPathOnly.replace(/^\/+/, '').replace(/\/+$/, ''); const trimmedHashPath = hashPathOnly.replace(/^\/+/, '').replace(/\/+$/, '');