fix: clean failed document uploads and surface filesystem errors
This commit is contained in:
@@ -199,3 +199,10 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
|
||||
- Yükleme ve üst veri güncellemesinde gerçek takvim günü doğrulanıyor; 31 Şubat, artık yıl olmayan 29 Şubat, tarih sonuna eklenen metin ve yanlış türler reddediliyor.
|
||||
- Dosya aktarımının base64 kodlaması ve 10 MB dosya sınırı doğrulanıyor. Bozuk kodlamanın boş veya kısmen çözülmüş dosya olarak kaydedilmesi önlendi. Bu kontrol PDF/DOCX iç yapısının sağlam olduğunu kanıtlamaz; ayrıştırma/OCR hata senaryoları açık.
|
||||
- Kanıt: genişletilen HTTP testleriyle 55/55 test geçti; bozuk yükleme ve tarih isteği 400, diğer hesabın tarih silmesi 404, geçerli silme ve kategori koruma doğrulandı. 10 MB tam sınırı ve bir bayt fazlası ayrıca gerçek Buffer ile geçti. Gerçek dosya indirme ve bayt eşitliği regresyonu geçti. Tarayıcıda ardışık tarih güncelleme yarışı ayrıca incelenmeli.
|
||||
|
||||
### D01 — Dosya kaydı başarısızlığında temizlik
|
||||
|
||||
- Yeni yükleme tek servis üzerinden benzersiz ad ve exclusive write ile yazılıyor; metadata INSERT başarısızsa yalnız bu yüklemenin baytları siliniyor. Temizlik de başarısızsa ayrı hata veriliyor. Önceden mevcut dosyalara dokunulmuyor; yeni dosyanın izinleri 0600.
|
||||
- Fiziksel silme sırasında yalnız ENOENT (zaten yok) tolere ediliyor. Erişim/tür/disk hataları artık bastırılmıyor; belge metadata kaydı silinmeden hata dönüyor.
|
||||
- Kanıt: 57/57 test geçti. SQLite trigger ile gerçek INSERT hatası üretildi; dosya listesi değişmedi ve önceden kayıtlı Türkçe belge baytları korundu. Dizin üzerinde unlink hatasıyla metadata koruması ve diğer hesabın silme reddi doğrulandı. Mevcut HTTP yükleme/indirme testi geçti.
|
||||
- Kalan sınır: süreç aniden kapanırken dosya/DB arasında tam atomiklik ve fiziksel silme sonrasındaki DB silme hatasının kurtarılması henüz çözülmüş değildir. Bunlar dosya yaşam döngüsü kapsamında açık tutuluyor; tam belge dayanıklılığı kabulü verilmedi.
|
||||
|
||||
+2
-6
@@ -969,17 +969,13 @@ const server = http.createServer(async (req, res) => {
|
||||
try { ofisMetni = arsivMetni(String(body.ad), buf); } catch { /* metin çıkmazsa devam */ }
|
||||
if (!ikn && body.otoIkn && ofisMetni) ikn = iknCikar(ofisMetni);
|
||||
}
|
||||
const dosyaAdi = `${Date.now()}-${crypto.randomUUID()}`;
|
||||
fs.writeFileSync(path.join(store.BELGE_DIR, dosyaAdi), buf);
|
||||
const id = store.addDocument(user.email, {
|
||||
const id = store.saveDocumentFile(user.email, {
|
||||
ad: String(body.ad).slice(0, 200),
|
||||
kategori: metadata.kategori || null,
|
||||
boyut: buf.length, // GERÇEK bayt — rastgele değil
|
||||
mime: body.mime || null,
|
||||
dosya: dosyaAdi,
|
||||
sonKullanma: metadata.sonKullanma || null,
|
||||
ikn, // ihaleye bağlı belge
|
||||
});
|
||||
}, buf);
|
||||
// Yükleme anında otomatik kalem çıkarımı — AI kullanmaz, kullanıcıdan
|
||||
// tıklama beklemez. PDF'ler tarayıcıda çözüldüğü için istemci
|
||||
// metni /api/kalem/cikar'a gönderir.
|
||||
|
||||
+17
-1
@@ -249,6 +249,21 @@ export function addDocument(email, { ad, kategori, boyut, mime, dosya, sonKullan
|
||||
return id;
|
||||
}
|
||||
|
||||
/** A rejected metadata insert must not leave private uploaded bytes on disk. */
|
||||
export function saveDocumentFile(email, metadata, bytes) {
|
||||
fs.mkdirSync(BELGE_DIR, { recursive:true });
|
||||
const dosya = crypto.randomUUID();
|
||||
const destination = path.join(BELGE_DIR, dosya);
|
||||
fs.writeFileSync(destination, bytes, { flag:'wx', mode:0o600 });
|
||||
try {
|
||||
return addDocument(email, { ...metadata, dosya, boyut:bytes.length });
|
||||
} catch (error) {
|
||||
try { fs.unlinkSync(destination); }
|
||||
catch (cleanupError) { throw new AggregateError([error,cleanupError], 'Belge kaydedilemedi ve geçici dosya temizlenemedi.'); }
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function listDocuments(email, { ikn } = {}) {
|
||||
const alanlar = 'id, ad, kategori, boyut, mime, son_kullanma, ikn, olusturma, dosya';
|
||||
return withDb((db) => (ikn
|
||||
@@ -280,7 +295,8 @@ export function listUsers() {
|
||||
export function deleteDocument(email, id) {
|
||||
const rec = getDocument(email, id);
|
||||
if (!rec) return false;
|
||||
try { fs.unlinkSync(path.join(BELGE_DIR, rec.dosya)); } catch { /* dosya zaten yok */ }
|
||||
try { fs.unlinkSync(path.join(BELGE_DIR, rec.dosya)); }
|
||||
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
||||
return withDb((db) => db.prepare('DELETE FROM belgeler WHERE id=? AND email=?').run(id, email).changes > 0, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
const dir = fs.mkdtempSync(join(tmpdir(),'ihaleist-doc-failure-'));
|
||||
process.env.IHALEIST_USER_DATA_DIR = dir;
|
||||
const store = await import('../server/user-store.mjs');
|
||||
after(()=>fs.rmSync(dir,{recursive:true,force:true}));
|
||||
|
||||
test('failed document insert removes only the new upload and preserves previous files',()=>{
|
||||
const bytes=Buffer.from('Özel test belgesi');
|
||||
const id=store.saveDocumentFile('a',{ad:'test.txt',mime:'text/plain'},bytes);
|
||||
const record=store.getDocument('a',id);
|
||||
const before=fs.readdirSync(store.BELGE_DIR);
|
||||
const db=store.openDb();
|
||||
db.exec("CREATE TRIGGER reject_document BEFORE INSERT ON belgeler BEGIN SELECT RAISE(FAIL,'test insert rejected'); END;");
|
||||
db.close();
|
||||
assert.throws(()=>store.saveDocumentFile('a',{ad:'failed.txt'},bytes),/test insert rejected/);
|
||||
assert.deepEqual(fs.readdirSync(store.BELGE_DIR),before);
|
||||
assert.deepEqual(fs.readFileSync(join(store.BELGE_DIR,record.dosya)),bytes);
|
||||
assert.equal(store.listDocuments('a').length,1);
|
||||
});
|
||||
|
||||
test('filesystem deletion error preserves document metadata instead of reporting success',()=>{
|
||||
const destination=join(store.BELGE_DIR,'not-a-file');
|
||||
fs.mkdirSync(destination);
|
||||
const db=store.openDb(); db.exec('DROP TRIGGER reject_document'); db.close();
|
||||
const id=store.addDocument('a',{ad:'test',boyut:1,dosya:'not-a-file'});
|
||||
assert.throws(()=>store.deleteDocument('a',id));
|
||||
assert.ok(store.getDocument('a',id));
|
||||
assert.equal(store.deleteDocument('b',id),false);
|
||||
fs.rmdirSync(destination);
|
||||
assert.equal(store.deleteDocument('a',id),true);
|
||||
});
|
||||
Reference in New Issue
Block a user