Archive v0.1.0 — 사진/영상 라이브러리 관리 프로그램
Tauri v2 + SolidJS + SQLite + ffmpeg 사이드카로 구현한 크로스플랫폼 사진·영상 관리 앱. 로컬/NAS(SFTP·WebDAV·FTP) 소스, 가상 그리드, 인앱 재생, 태그/이동/삭제/undo, 중복 탐지, 포터블 배포. - archive-db: SQLite 스키마·마이그레이션·단일 writer 스레드 + FTS5 trigram - archive-vfs: VFS 4백엔드(local/sftp/ftp/webdav) + 자격증명(키체인/볼트) - archive-indexer: 스캔·해시·썸네일·중복탐지·태그·파일작업·유지보수 - archive-media: localhost HTTP 미디어 서버(Range) + ffmpeg 스트림 잡 - 프론트: 3-pane UI, justified 가상 그리드, 라이트박스, 중복 검토 패널 Rust 테스트 49개 통과. CI: win x64/arm64 포터블 zip + macOS universal dmg. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# WebView2 CDP (Chrome DevTools Protocol) E2E helper.
|
||||
# Usage: . .\scripts\cdp.ps1; Invoke-Cdp "document.title"
|
||||
# Prereq: app launched with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9223
|
||||
# NOTE: ASCII only in this file (PS 5.1 reads BOM-less files as ANSI).
|
||||
|
||||
function Invoke-Cdp {
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$Expression,
|
||||
[int]$Port = 9223,
|
||||
[int]$TimeoutSec = 20
|
||||
)
|
||||
$targets = Invoke-RestMethod "http://127.0.0.1:$Port/json"
|
||||
$page = $targets | Where-Object { $_.type -eq 'page' -and $_.url -notlike 'devtools*' } | Select-Object -First 1
|
||||
if (-not $page) { throw "no CDP page target" }
|
||||
|
||||
$ws = New-Object System.Net.WebSockets.ClientWebSocket
|
||||
$ct = [System.Threading.CancellationToken]::None
|
||||
$uri = [Uri]$page.webSocketDebuggerUrl
|
||||
$ws.ConnectAsync($uri, $ct).GetAwaiter().GetResult() | Out-Null
|
||||
|
||||
$msg = @{
|
||||
id = 1
|
||||
method = "Runtime.evaluate"
|
||||
params = @{
|
||||
expression = $Expression
|
||||
returnByValue = $true
|
||||
awaitPromise = $true
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
|
||||
$seg = New-Object System.ArraySegment[byte] -ArgumentList @(,$bytes)
|
||||
$ws.SendAsync($seg, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult() | Out-Null
|
||||
|
||||
$buffer = New-Object byte[] 1048576
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
$response = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
do {
|
||||
$rseg = New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)
|
||||
$result = $ws.ReceiveAsync($rseg, $ct).GetAwaiter().GetResult()
|
||||
$sb.Append([System.Text.Encoding]::UTF8.GetString($buffer, 0, $result.Count)) | Out-Null
|
||||
} while (-not $result.EndOfMessage)
|
||||
$obj = $sb.ToString() | ConvertFrom-Json
|
||||
if ($obj.id -eq 1) { $response = $obj; break }
|
||||
}
|
||||
$ws.Abort()
|
||||
if (-not $response) { throw "CDP response timeout" }
|
||||
if ($response.result.exceptionDetails) {
|
||||
throw "JS exception: $($response.result.exceptionDetails.text) $($response.result.exceptionDetails.exception.description)"
|
||||
}
|
||||
return $response.result.result.value
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
rem Archive 개발 실행 — rustc가 라이브러리 없는 VS Community를 잡지 않도록
|
||||
rem Build Tools vcvars64 환경에서 tauri dev를 띄운다.
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul
|
||||
cd /d "%~dp0.."
|
||||
npm run tauri dev %*
|
||||
@@ -0,0 +1,34 @@
|
||||
# macOS용 ffmpeg/ffprobe 사이드카 — arm64 + x64를 받아 lipo로 universal 생성.
|
||||
# CI(macos-14)에서 실행. 실패 시 개별 아키텍처 바이너리라도 배치한다.
|
||||
# 참고: Martin Riedl 빌드(ffmpeg.martin-riedl.de) — 서명·공증됨.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$binDir = Join-Path $root "src-tauri/binaries"
|
||||
New-Item -ItemType Directory -Force $binDir | Out-Null
|
||||
|
||||
# Riedl 최신 릴리스 API (arm64/amd64 각각 ffmpeg/ffprobe zip)
|
||||
$api = "https://ffmpeg.martin-riedl.de/api/v1/latest"
|
||||
$release = Invoke-RestMethod $api
|
||||
|
||||
function Get-Tool($arch, $tool) {
|
||||
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) "ff-$arch-$tool"
|
||||
New-Item -ItemType Directory -Force $tmp | Out-Null
|
||||
$entry = $release.$tool.$arch
|
||||
if (-not $entry) { throw "$tool/$arch 다운로드 정보 없음" }
|
||||
$zip = Join-Path $tmp "$tool.zip"
|
||||
Invoke-WebRequest $entry.url -OutFile $zip
|
||||
Expand-Archive $zip -DestinationPath $tmp -Force
|
||||
$bin = Get-ChildItem $tmp -Recurse -Filter $tool | Select-Object -First 1
|
||||
return $bin.FullName
|
||||
}
|
||||
|
||||
foreach ($tool in @("ffmpeg", "ffprobe")) {
|
||||
$arm = Get-Tool "arm64" $tool
|
||||
$x64 = Get-Tool "amd64" $tool
|
||||
$out = Join-Path $binDir "$tool-universal-apple-darwin"
|
||||
# lipo로 universal 결합
|
||||
& lipo -create $arm $x64 -output $out
|
||||
& chmod +x $out
|
||||
Write-Host "[fetch-ffmpeg-macos] $out (universal)"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# ffmpeg/ffprobe 사이드카 다운로드 (BtbN FFmpeg-Builds, LGPL 정적 빌드)
|
||||
# 사용: .\scripts\fetch-ffmpeg.ps1 [-Target win64|winarm64]
|
||||
# 결과: src-tauri\binaries\ffmpeg-<triple>.exe, ffprobe-<triple>.exe + manifest
|
||||
param(
|
||||
[ValidateSet("win64", "winarm64")]
|
||||
[string]$Target = "win64"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = "BtbN/FFmpeg-Builds"
|
||||
$triple = if ($Target -eq "win64") { "x86_64-pc-windows-msvc" } else { "aarch64-pc-windows-msvc" }
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$binDir = Join-Path $root "src-tauri\binaries"
|
||||
New-Item -ItemType Directory -Force $binDir | Out-Null
|
||||
|
||||
if ((Test-Path "$binDir\ffmpeg-$triple.exe") -and (Test-Path "$binDir\ffprobe-$triple.exe")) {
|
||||
Write-Host "[fetch-ffmpeg] 이미 존재: $triple — 건너뜀 (강제 갱신은 파일 삭제 후 재실행)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "[fetch-ffmpeg] BtbN 'latest' 릴리스 자산 조회..."
|
||||
$release = Invoke-RestMethod "https://api.github.com/repos/$repo/releases/tags/latest" -Headers @{ "User-Agent" = "archive-build" }
|
||||
|
||||
# 버전 브랜치(nX.Y) LGPL 정적 zip 우선, 없으면 master
|
||||
$pattern = "^ffmpeg-n[\d\.]+-latest-$Target-lgpl-[\d\.]+\.zip$"
|
||||
$asset = $release.assets | Where-Object { $_.name -match $pattern } | Sort-Object name -Descending | Select-Object -First 1
|
||||
if (-not $asset) {
|
||||
$asset = $release.assets | Where-Object { $_.name -eq "ffmpeg-master-latest-$Target-lgpl.zip" } | Select-Object -First 1
|
||||
}
|
||||
if (-not $asset) { throw "LGPL $Target 자산을 찾을 수 없습니다" }
|
||||
|
||||
Write-Host "[fetch-ffmpeg] 다운로드: $($asset.name) ($([math]::Round($asset.size / 1MB, 1)) MB)"
|
||||
$tmp = Join-Path $env:TEMP "ffmpeg-fetch"
|
||||
New-Item -ItemType Directory -Force $tmp | Out-Null
|
||||
$zipPath = Join-Path $tmp $asset.name
|
||||
Invoke-WebRequest $asset.browser_download_url -OutFile $zipPath -UseBasicParsing
|
||||
|
||||
$sha256 = (Get-FileHash $zipPath -Algorithm SHA256).Hash
|
||||
Write-Host "[fetch-ffmpeg] SHA256: $sha256"
|
||||
|
||||
$extract = Join-Path $tmp "extract"
|
||||
Remove-Item -Recurse -Force $extract -ErrorAction SilentlyContinue
|
||||
Expand-Archive $zipPath -DestinationPath $extract
|
||||
|
||||
$ffmpeg = Get-ChildItem $extract -Recurse -Filter "ffmpeg.exe" | Select-Object -First 1
|
||||
$ffprobe = Get-ChildItem $extract -Recurse -Filter "ffprobe.exe" | Select-Object -First 1
|
||||
if (-not $ffmpeg -or -not $ffprobe) { throw "zip 안에서 ffmpeg/ffprobe.exe를 찾지 못함" }
|
||||
|
||||
Copy-Item $ffmpeg.FullName "$binDir\ffmpeg-$triple.exe" -Force
|
||||
Copy-Item $ffprobe.FullName "$binDir\ffprobe-$triple.exe" -Force
|
||||
|
||||
@{
|
||||
asset = $asset.name
|
||||
sha256 = $sha256
|
||||
target = $Target
|
||||
triple = $triple
|
||||
fetched = (Get-Date).ToString("o")
|
||||
} | ConvertTo-Json | Set-Content "$binDir\manifest-$Target.json" -Encoding utf8
|
||||
|
||||
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
||||
Write-Host "[fetch-ffmpeg] 완료: $binDir\ffmpeg-$triple.exe"
|
||||
@@ -0,0 +1,62 @@
|
||||
# Windows 포터블 zip 패키징.
|
||||
# 사용: .\scripts\package-portable.ps1 [-Target x64|arm64]
|
||||
# 결과: dist-portable\Archive-<ver>-windows-<arch>.zip
|
||||
# 내용: Archive.exe, ffmpeg.exe, ffprobe.exe, data\(빈 폴더 → 포터블 모드), README.txt
|
||||
param(
|
||||
[ValidateSet("x64", "arm64")]
|
||||
[string]$Target = "x64"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$triple = if ($Target -eq "x64") { "x86_64-pc-windows-msvc" } else { "aarch64-pc-windows-msvc" }
|
||||
|
||||
# 버전 (tauri.conf.json)
|
||||
$conf = Get-Content "$root\src-tauri\tauri.conf.json" -Raw | ConvertFrom-Json
|
||||
$ver = $conf.version
|
||||
|
||||
$exe = if ($Target -eq "x64") {
|
||||
"$root\src-tauri\target\release\archive.exe"
|
||||
} else {
|
||||
"$root\src-tauri\target\$triple\release\archive.exe"
|
||||
}
|
||||
if (-not (Test-Path $exe)) {
|
||||
throw "빌드 산출물이 없습니다: $exe (먼저 tauri build --no-bundle 실행)"
|
||||
}
|
||||
|
||||
$ffmpeg = "$root\src-tauri\binaries\ffmpeg-$triple.exe"
|
||||
$ffprobe = "$root\src-tauri\binaries\ffprobe-$triple.exe"
|
||||
if (-not (Test-Path $ffmpeg)) {
|
||||
throw "ffmpeg 사이드카 없음: $ffmpeg (scripts\fetch-ffmpeg.ps1 -Target win$($Target -replace 'x','') 먼저 실행)"
|
||||
}
|
||||
|
||||
$stage = "$root\dist-portable\stage-$Target"
|
||||
Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force "$stage\data" | Out-Null
|
||||
|
||||
Copy-Item $exe "$stage\Archive.exe"
|
||||
Copy-Item $ffmpeg "$stage\ffmpeg.exe"
|
||||
Copy-Item $ffprobe "$stage\ffprobe.exe"
|
||||
|
||||
@"
|
||||
Archive $ver — 포터블 (무설치)
|
||||
|
||||
실행: Archive.exe 를 더블클릭하세요.
|
||||
|
||||
- 이 폴더의 data\ 하위에 인덱스(archive.db)와 썸네일이 저장됩니다.
|
||||
폴더째로 USB/다른 PC로 옮겨도 그대로 동작합니다.
|
||||
- WebView2 런타임이 필요합니다. Windows 11과 최신 Windows 10에는
|
||||
기본 설치되어 있습니다. 없다면 아래에서 설치하세요:
|
||||
https://developer.microsoft.com/microsoft-edge/webview2/
|
||||
|
||||
문제가 있으면 data\logs\ 의 로그를 확인하세요.
|
||||
"@ | Set-Content "$stage\README.txt" -Encoding utf8
|
||||
|
||||
$outDir = "$root\dist-portable"
|
||||
$zip = "$outDir\Archive-$ver-windows-$Target.zip"
|
||||
Remove-Item $zip -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path "$stage\*" -DestinationPath $zip -CompressionLevel Optimal
|
||||
Remove-Item -Recurse -Force $stage
|
||||
|
||||
$sizeMB = [math]::Round((Get-Item $zip).Length / 1MB, 1)
|
||||
Write-Host "[package-portable] 완료: $zip ($sizeMB MB)"
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
rem 코어 크레이트 헤드리스 테스트 (Build Tools vcvars64 환경)
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul
|
||||
cd /d "%~dp0..\src-tauri"
|
||||
cargo test --workspace %*
|
||||
Reference in New Issue
Block a user