Replace logbook backup v1 JSON with v2 ZIP archives.
ZIP .daagbok files use a compact manifest and binary KDAB blobs so large photo, voice, and GPS payloads no longer inflate in a single JSON file. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
export const BACKUP_FORMAT = 'kapteins-daagbok-backup' as const
|
||||
export const BACKUP_VERSION = 2 as const
|
||||
|
||||
export interface BackupIndexedFile {
|
||||
path: string
|
||||
updatedAt: string
|
||||
bytes: number
|
||||
}
|
||||
|
||||
export interface BackupIndexedPayloadFile extends BackupIndexedFile {
|
||||
payloadId: string
|
||||
}
|
||||
|
||||
export interface BackupIndexedEntryFile extends BackupIndexedPayloadFile {
|
||||
entryId: string
|
||||
}
|
||||
|
||||
export interface BackupIndexedTrackFile extends BackupIndexedFile {
|
||||
entryId: string
|
||||
}
|
||||
|
||||
export interface BackupManifestCounts {
|
||||
entries: number
|
||||
photos: number
|
||||
voiceMemos: number
|
||||
crews: number
|
||||
gpsTracks: number
|
||||
nmeaArchives: number
|
||||
hasYacht: boolean
|
||||
hasDeviation: boolean
|
||||
hasLogbookCrewSelection: boolean
|
||||
hasLogbookVesselSelection: boolean
|
||||
}
|
||||
|
||||
export interface BackupManifestFiles {
|
||||
key: string
|
||||
logbook: string
|
||||
yacht: string | null
|
||||
deviation: string | null
|
||||
logbookCrewSelection: string | null
|
||||
logbookVesselSelection: string | null
|
||||
crews: BackupIndexedPayloadFile[]
|
||||
entries: BackupIndexedPayloadFile[]
|
||||
photos: BackupIndexedEntryFile[]
|
||||
voiceMemos: BackupIndexedEntryFile[]
|
||||
gpsTracks: BackupIndexedTrackFile[]
|
||||
nmeaArchives: BackupIndexedTrackFile[]
|
||||
}
|
||||
|
||||
export interface BackupManifestV2 {
|
||||
format: typeof BACKUP_FORMAT
|
||||
version: typeof BACKUP_VERSION
|
||||
exportedAt: string
|
||||
appVersion?: string
|
||||
compression: 'zip-deflate-6'
|
||||
logbookId: string
|
||||
counts: BackupManifestCounts
|
||||
totalUncompressedBytes: number
|
||||
files: BackupManifestFiles
|
||||
}
|
||||
|
||||
export interface LogbookMetaJson {
|
||||
id: string
|
||||
encryptedTitle: string
|
||||
updatedAt: string
|
||||
isDemo?: boolean
|
||||
}
|
||||
|
||||
export function parseManifestJson(text: string): BackupManifestV2 {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
} catch {
|
||||
throw new Error('BACKUP_INVALID_FORMAT')
|
||||
}
|
||||
if (!isBackupManifestV2(parsed)) {
|
||||
throw new Error('BACKUP_INVALID_FORMAT')
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
export function isBackupManifestV2(value: unknown): value is BackupManifestV2 {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const obj = value as Partial<BackupManifestV2>
|
||||
return (
|
||||
obj.format === BACKUP_FORMAT &&
|
||||
obj.version === BACKUP_VERSION &&
|
||||
typeof obj.exportedAt === 'string' &&
|
||||
typeof obj.logbookId === 'string' &&
|
||||
!!obj.counts &&
|
||||
!!obj.files
|
||||
)
|
||||
}
|
||||
|
||||
export function serializeManifest(manifest: BackupManifestV2): string {
|
||||
return JSON.stringify(manifest)
|
||||
}
|
||||
Reference in New Issue
Block a user