53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import type { MediaItem } from '../types';
|
|
|
|
/** Trigger a browser download of a single media item by URL. */
|
|
export function downloadMedia(item: MediaItem): void {
|
|
if (typeof document === 'undefined') return;
|
|
const a = document.createElement('a');
|
|
a.href = item.src;
|
|
a.download = sanitizeFilename(item.name);
|
|
a.rel = 'noopener';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|
|
|
|
/** Export a metadata-only JSON sidecar for the current selection. */
|
|
export function exportMetadata(items: MediaItem[]): void {
|
|
if (typeof document === 'undefined') return;
|
|
const payload = items.map((i) => ({
|
|
name: i.name,
|
|
takenAt: new Date(i.takenAt).toISOString(),
|
|
source: i.source,
|
|
tags: i.tags,
|
|
objects: i.objectLabels,
|
|
location: i.location,
|
|
favorite: i.favorite,
|
|
width: i.width,
|
|
height: i.height,
|
|
}));
|
|
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'photos-metadata.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
}
|
|
|
|
/**
|
|
* Sanitize a filename to prevent path traversal / illegal characters when it is
|
|
* used as a download target. Security-relevant: never trust an imported name.
|
|
*/
|
|
export function sanitizeFilename(name: string): string {
|
|
const cleaned = name
|
|
// Strip path separators and characters illegal on common filesystems.
|
|
.replace(/[/\\?%*:|"<>]/g, '_')
|
|
// Collapse parent-directory sequences (path traversal guard).
|
|
.replace(/\.\.+/g, '_')
|
|
.trim();
|
|
return cleaned.slice(0, 200) || 'photo';
|
|
}
|