import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk'; import { glob } from 'glob'; import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; import { readFile, stat, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import yaml from 'yaml'; export interface BaseOptions { configDirectory: string; apiKey?: string; instanceUrl?: string; } export interface AuthDto { instanceUrl: string; apiKey: string; } export const authenticate = async (options: BaseOptions): Promise => { const { configDirectory: configDir, instanceUrl, apiKey } = options; // provided in command if (instanceUrl && apiKey) { await connect(instanceUrl, apiKey); return; } // fallback to file const config = await readAuthFile(configDir); await connect(config.instanceUrl, config.apiKey); }; export const connect = async (instanceUrl: string, apiKey: string): Promise => { const wellKnownUrl = new URL('.well-known/immich', instanceUrl); try { const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString(); if (endpoint !== instanceUrl) { console.debug(`Discovered API at ${endpoint}`); } instanceUrl = endpoint; } catch { // noop } defaults.baseUrl = instanceUrl; defaults.headers = { 'x-api-key': apiKey }; const [error] = await withError(getMyUserInfo()); if (isHttpError(error)) { logError(error, 'Failed to connect to server'); process.exit(1); } }; export const logError = (error: unknown, message: string) => { if (isHttpError(error)) { console.error(`${message}: ${error.status}`); console.error(JSON.stringify(error.data, undefined, 2)); } else { console.error(`${message} - ${error}`); } }; export const getAuthFilePath = (dir: string) => join(dir, 'auth.yml'); export const readAuthFile = async (dir: string) => { try { const data = await readFile(getAuthFilePath(dir)); // TODO add class-transform/validation return yaml.parse(data.toString()) as AuthDto; } catch (error: Error | any) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { console.log('No auth file exists. Please login first.'); process.exit(1); } throw error; } }; export const writeAuthFile = async (dir: string, auth: AuthDto) => writeFile(getAuthFilePath(dir), yaml.stringify(auth), { mode: 0o600 }); export const withError = async (promise: Promise): Promise<[Error, undefined] | [undefined, T]> => { try { const result = await promise; return [undefined, result]; } catch (error: Error | any) { return [error, undefined]; } }; export interface CrawlOptions { pathsToCrawl: string[]; recursive?: boolean; includeHidden?: boolean; exclusionPatterns?: string[]; extensions: string[]; } export const crawl = async (options: CrawlOptions): Promise => { const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options; const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', '')); if (pathsToCrawl.length === 0) { return []; } const patterns: string[] = []; const crawledFiles: string[] = []; for await (const currentPath of pathsToCrawl) { try { const stats = await stat(currentPath); if (stats.isFile() || stats.isSymbolicLink()) { crawledFiles.push(currentPath); } else { patterns.push(currentPath); } } catch (error: any) { if (error.code === 'ENOENT') { patterns.push(currentPath); } else { throw error; } } } let searchPattern: string; if (patterns.length === 1) { searchPattern = patterns[0]; } else if (patterns.length === 0) { return crawledFiles; } else { searchPattern = '{' + patterns.join(',') + '}'; } if (recursive) { searchPattern = searchPattern + '/**/'; } searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`; const globbedFiles = await glob(searchPattern, { absolute: true, nocase: true, nodir: true, dot: includeHidden, ignore: exclusionPatterns, }); return [...crawledFiles, ...globbedFiles].sort(); }; export const sha1 = (filepath: string) => { const hash = createHash('sha1'); return new Promise((resolve, reject) => { const rs = createReadStream(filepath); rs.on('error', reject); rs.on('data', (chunk) => hash.update(chunk)); rs.on('end', () => resolve(hash.digest('hex'))); }); };