Replace project with current server release and Windows setup

Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
This commit is contained in:
2026-10-09 16:43:35 +09:00
parent 71c8d3b7ff
commit e358e24436
642 changed files with 35202 additions and 17642 deletions
+12
View File
@@ -0,0 +1,12 @@
import assert from'node:assert/strict';import{test}from'node:test';import{loadTypescript}from'./support/load-typescript.mjs';
const load=()=>loadTypescript(new URL('../src/input/PhotoMetadata.ts',import.meta.url));
function jpeg(orientation,width=4032,height=3024){const bytes=new Uint8Array(51),v=new DataView(bytes.buffer);bytes.set([255,216,255,225,0,34,69,120,105,102,0,0,73,73,42,0,8,0,0,0,1,0,18,1,3,0,1,0,0,0,orientation,0,0,0,0,0,0,0,255,192,0,11,8],0);v.setUint16(43,height);v.setUint16(45,width);bytes.set([1,1,17,0],47);return bytes;}
test('JPEG EXIF1–8 dimensions swap only on transpose orientations',async()=>{const{readPhotoMetadata}=await load();for(let o=1;o<=8;o++){const m=await readPhotoMetadata(new Blob([jpeg(o)]));assert.equal(m.orientation,o);assert.deepEqual([m.width,m.height],o>=5?[3024,4032]:[4032,3024]);}});
test('PNG and WebP header dimensions do not decode full image',async()=>{const{readPhotoMetadata}=await load();const p=new Uint8Array(24);p.set([137,80,78,71,13,10,26,10]);new DataView(p.buffer).setUint32(16,8000);new DataView(p.buffer).setUint32(20,6000);assert.equal((await readPhotoMetadata(new Blob([p]))).width,8000);const w=new Uint8Array(30);w.set(new TextEncoder().encode('RIFFxxxxWEBPVP8X'));w[24]=255;w[25]=15;w[27]=255;w[28]=7;assert.deepEqual(await readPhotoMetadata(new Blob([w])),{width:4096,height:2048,orientation:1});});
test('truncated/malformed metadata safely falls back and read budget is bounded',async()=>{const{readPhotoMetadata,PHOTO_HEADER_BYTES}=await load();assert.equal(await readPhotoMetadata(new Blob([new Uint8Array(3)])),null);let length;await readPhotoMetadata({slice(start,end){length=end-start;return new Blob([new Uint8Array(3)]);}});assert.ok(length<=256*1024);assert.equal(length,PHOTO_HEADER_BYTES);});
test('known large file requests its canonical dimensions on the first decoder invocation',async()=>{const old=globalThis.createImageBitmap,calls=[];globalThis.createImageBitmap=async(_source,opts)=>{calls.push(opts);return{width:opts.resizeWidth,height:opts.resizeHeight,close(){}};};try{const{createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const result=await createPhotoSnapshot(new Blob([jpeg(6)]),1,new AbortController().signal);assert.equal(calls.length,1);assert.deepEqual([calls[0].resizeWidth,calls[0].resizeHeight],[768,1024]);assert.deepEqual([result.width,result.height],[768,1024]);}finally{globalThis.createImageBitmap=old;}});
test('unknown PNG/WebP orientation never forces raw-aspect decoder resize',async()=>{const old=globalThis.createImageBitmap,calls=[];const p=new Uint8Array(24);p.set([137,80,78,71,13,10,26,10]);new DataView(p.buffer).setUint32(16,4032);new DataView(p.buffer).setUint32(20,3024);globalThis.createImageBitmap=async(_source,opts)=>{calls.push(opts);return calls.length===1?{width:3024,height:4032,close(){}}:{width:768,height:1024,close(){}};};try{const{createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const result=await createPhotoSnapshot(new Blob([p]),1,new AbortController().signal);assert.equal(calls[0].resizeWidth,undefined);assert.equal(calls[1].resizeWidth,768);assert.equal(result.width,768);}finally{globalThis.createImageBitmap=old;}});
function tiff(o){const b=new Uint8Array(26),v=new DataView(b.buffer);b.set([73,73,42,0,8,0,0,0,1,0,18,1,3,0,1,0,0,0,o]);return b;}
function chunk(type,data,png){const b=new Uint8Array(8+data.length+(png?4:data.length%2)),v=new DataView(b.buffer);b.set(new TextEncoder().encode(type),png?4:0);v.setUint32(png?0:4,data.length,!png);b.set(data,8);return b;}
test('PNG/WebP EXIF after large encoded payload is read with bounded slices for rotations and mirrors',async()=>{const{readPhotoMetadata,PHOTO_HEADER_BYTES}=await load();for(const png of[true,false])for(let o=1;o<=8;o++){let bytes;if(png){const ihdr=new Uint8Array(13),v=new DataView(ihdr.buffer);v.setUint32(0,8000);v.setUint32(4,6000);bytes=new Blob([new Uint8Array([137,80,78,71,13,10,26,10]),chunk('IHDR',ihdr,true),chunk('IDAT',new Uint8Array(1024*1024),true),chunk('eXIf',tiff(o),true),chunk('IEND',new Uint8Array(),true)]);}else{const vp8x=new Uint8Array(10);vp8x[0]=8;vp8x[4]=63;vp8x[5]=31;vp8x[7]=111;vp8x[8]=23;const body=new Blob([chunk('VP8X',vp8x,false),chunk('VP8 ',new Uint8Array(1024*1024),false),chunk('EXIF',tiff(o),false)]),head=new Uint8Array(12);head.set(new TextEncoder().encode('RIFF'));new DataView(head.buffer).setUint32(4,body.size+4,true);head.set(new TextEncoder().encode('WEBP'),8);bytes=new Blob([head,body]);}let read=0;const file={size:bytes.size,slice(start,end){read+=end-start;return bytes.slice(start,end);}};const metadata=await readPhotoMetadata(file);assert.deepEqual(metadata,{width:o>=5?6000:8000,height:o>=5?8000:6000,orientation:o});assert.ok(read<=2*PHOTO_HEADER_BYTES);}});
test('JPEG without EXIF positively records orientation1 when scan reaches image data',async()=>{const{readPhotoMetadata}=await load(),b=new Uint8Array([255,216,255,192,0,11,8,11,208,15,160,1,1,17,0,255,218,0,2]);assert.deepEqual(await readPhotoMetadata(new Blob([b])),{width:4000,height:3024,orientation:1});});