Initial commit: AI Photo Organizer (Electron + face-api)
Local-first photo organizer that auto-sorts images by face recognition and EXIF capture date. - Electron app with 3-process split: Main (Node) / UI Renderer (React) / hidden Inference Renderer (face-api + WebGL) - Core pipeline: scan -> face match + EXIF -> path build -> atomic move/copy - Move = copy -> verify -> delete; auto-rename on filename collision - 1st-registered profile = move, others = copy; unmatched -> [미정]/YYYY/MM - EXIF date with mtime fallback - Vitest unit tests (pathBuilder / fileOps / concurrency) all green - electron-builder config for Windows (nsis) + macOS (dmg) - Docs: PRD / DECISIONS / ARCHITECTURE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtemp, writeFile, readFile, rm, mkdir, access } from 'node:fs/promises'
|
||||
import { constants } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { safeMove, safeCopy, resolveCollisionFreePath } from '../src/main/fileOps'
|
||||
|
||||
let dir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'photoai-'))
|
||||
})
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function exists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await access(p, constants.F_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
describe('safeMove', () => {
|
||||
it('복사 후 원본을 삭제하고 내용이 보존된다', async () => {
|
||||
const src = join(dir, 'a.txt')
|
||||
await writeFile(src, 'hello')
|
||||
const target = join(dir, 'out', 'a.txt')
|
||||
|
||||
const dest = await safeMove(src, target)
|
||||
expect(await exists(src)).toBe(false) // 원본 삭제됨
|
||||
expect(await readFile(dest, 'utf-8')).toBe('hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('safeCopy + 충돌 자동 리네임', () => {
|
||||
it('대상이 존재하면 _1, _2 로 리네임한다 (덮어쓰기 금지)', async () => {
|
||||
const target = join(dir, 'out', 'a.txt')
|
||||
await mkdir(join(dir, 'out'), { recursive: true })
|
||||
await writeFile(target, 'existing')
|
||||
|
||||
const src = join(dir, 'src.txt')
|
||||
await writeFile(src, 'new')
|
||||
|
||||
const dest = await safeCopy(src, target)
|
||||
expect(dest.endsWith('a_1.txt')).toBe(true)
|
||||
expect(await readFile(target, 'utf-8')).toBe('existing') // 원본 보존
|
||||
expect(await readFile(dest, 'utf-8')).toBe('new')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveCollisionFreePath', () => {
|
||||
it('충돌 없으면 그대로 반환한다', async () => {
|
||||
const target = join(dir, 'fresh.txt')
|
||||
expect(await resolveCollisionFreePath(target)).toBe(target)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user