UNPKG

backsplash-app

Version:
304 lines (261 loc) 9.8 kB
import { app } from "electron"; import * as fs from "fs"; import * as http from "http"; import * as https from "https"; import * as path from "path"; import log from "@/logger"; /** * Simple service for storing files locally */ export class FilesystemStorageService { private wallpaperDir: string; constructor() { this.wallpaperDir = path.join(app.getPath("userData"), "wallpapers"); this.ensureDirectoryExists(); } /** * Get the storage directory path */ public getStorageDir(): string { return this.wallpaperDir; } /** * Make sure the temp storage directory exists */ public ensureDirectoryExists(): void { if (!fs.existsSync(this.wallpaperDir)) { fs.mkdirSync(this.wallpaperDir, { recursive: true }); } } /** * Check if a file exists */ public fileExists(filePath: string): boolean { return fs.existsSync(filePath); } /** * Rename a file with error handling */ public renameFile(sourcePath: string, targetPath: string): boolean { try { fs.renameSync(sourcePath, targetPath); return true; } catch (error) { log.error(`Failed to rename file ${sourcePath} to ${targetPath}:`, error); return false; } } /** * Download a file from URL and store it */ public async downloadFile(url: string, filename: string): Promise<string> { const filePath = path.join(this.wallpaperDir, filename); return new Promise((resolve, reject) => { // Choose http or https module based on URL const httpClient = url.startsWith("https") ? https : http; const request = httpClient.get(url, (response) => { // Handle redirects if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (!redirectUrl) { return reject(new Error("Redirect location header missing")); } return this.downloadFile(redirectUrl, filename).then(resolve).catch(reject); } // Check for successful response if (response.statusCode !== 200) { return reject(new Error(`Failed to download file: ${response.statusCode}`)); } // Create write stream const fileStream = fs.createWriteStream(filePath); // Pipe the response to the file response.pipe(fileStream); // Handle completion fileStream.on("finish", () => { fileStream.close(); resolve(filePath); }); // Handle errors fileStream.on("error", (err) => { fs.unlink(filePath, () => null); // Delete the file if there was an error reject(err); }); }); // Handle request errors request.on("error", (err) => { reject(err); }); // End the request request.end(); }); } /** * Clean up old files in the storage directory, keeping only the most recent ones * @param pathsToKeep Array of specific paths to keep regardless of age * @param maxToKeep Maximum number of files to keep */ public cleanupOldFiles(pathsToKeep: string[] = [], maxToKeep = 10): void { if (!fs.existsSync(this.wallpaperDir)) { return; } try { const files = fs .readdirSync(this.wallpaperDir) .map((fileName) => { const filePath = path.join(this.wallpaperDir, fileName); try { return { name: fileName, path: filePath, mtime: fs.statSync(filePath).mtime.getTime(), }; } catch (err) { return null; // Skip files we can't stat } }) .filter((fileInfo): fileInfo is NonNullable<typeof fileInfo> => fileInfo !== null) .sort((a, b) => b.mtime - a.mtime); // Sort newest first // If we're already under the limit, do nothing if (files.length <= maxToKeep) { return; } // Files to delete = files beyond our keep limit const filesToDelete = files.slice(maxToKeep); filesToDelete.forEach((fileInfo) => { // Skip files we explicitly want to keep if (pathsToKeep.includes(fileInfo.path)) { return; } // Delete the file try { fs.unlinkSync(fileInfo.path); } catch (err) { log.error(`Failed to delete file: ${fileInfo.path}`, err); } }); } catch (error) { log.error(`Error during cleanup:`, error); } } /** * Legacy method for compatibility with old code * @deprecated Use cleanupOldFiles instead */ public cleanupOldWallpapers(): void { this.cleanupOldFiles(); } /** * Get information about the upscaled version of a wallpaper * @param objectKey The unique identifier for the wallpaper * @returns Object with upscaled filename and path (if exists) */ public getWallpaperUpscaledInfo(objectKey: string): { upscaledFilename: string; upscaledPath: string | null; } { const baseFilename = path.basename(objectKey); const ext = path.extname(baseFilename) || ".png"; // Default extension const nameWithoutExt = baseFilename.replace(ext, ""); const upscaledFilename = `${nameWithoutExt}_upscaled${ext}`; const upscaledPath = path.join(this.wallpaperDir, upscaledFilename); // Check if the upscaled file exists const upscaledExists = this.fileExists(upscaledPath); return { upscaledFilename, upscaledPath: upscaledExists ? upscaledPath : null, }; } /** * Get paths for both original and upscaled versions of a wallpaper * @param objectKey The unique identifier (e.g. S3 key/filename) for the wallpaper * @returns Object with paths for original and upscaled versions * @deprecated Use getWallpaperUpscaledInfo instead as we don't store original images locally */ public getWallpaperPaths(objectKey: string): { originalFilename: string; upscaledFilename: string; originalPath: string | null; upscaledPath: string | null; } { const baseFilename = path.basename(objectKey); const ext = path.extname(baseFilename) || ".png"; // Default extension const nameWithoutExt = baseFilename.replace(ext, ""); const upscaledFilename = `${nameWithoutExt}_upscaled${ext}`; const originalFilename = baseFilename; const upscaledPath = path.join(this.wallpaperDir, upscaledFilename); const originalPath = path.join(this.wallpaperDir, originalFilename); // Check if the files exist and return null for paths that don't exist const originalExists = this.fileExists(originalPath); const upscaledExists = this.fileExists(upscaledPath); return { originalFilename, upscaledFilename, originalPath: originalExists ? originalPath : null, upscaledPath: upscaledExists ? upscaledPath : null, }; } /** * Check if an upscaled version exists for a given wallpaper * @param objectKey The unique identifier for the wallpaper * @returns Boolean indicating if upscaled version exists and path if it does */ public hasUpscaledVersion(objectKey: string): { exists: boolean; path: string | null } { const { upscaledPath } = this.getWallpaperUpscaledInfo(objectKey); return { exists: upscaledPath !== null, path: upscaledPath, }; } /** * Ensure upscaled file is properly named according to our convention * @param currentPath Current path of the upscaled file * @param objectKey The object key for the wallpaper * @returns The final path where the upscaled file is located */ public normalizeUpscaledPath(currentPath: string, objectKey: string): string { const { upscaledPath } = this.getWallpaperUpscaledInfo(objectKey); const expectedPath = upscaledPath || path.join(this.wallpaperDir, this.getWallpaperUpscaledInfo(objectKey).upscaledFilename); // If files are different, try to rename if (currentPath !== expectedPath && this.fileExists(currentPath)) { if (this.renameFile(currentPath, expectedPath)) { log.info(`Renamed downloaded upscaled file to: ${expectedPath}`); return expectedPath; } } // Return original path if rename failed or wasn't needed return this.fileExists(expectedPath) ? expectedPath : currentPath; } /** * Copy a downloaded upscaled file to the naming convention for the original file * This ensures we have the upscaled version for the original file even if the API returns a different key * @param sourceObjectKey The object key returned by the upscale API * @param targetObjectKey The original object key we're upscaling for * @returns Path to the copied upscaled file or null if copy failed */ public copyUpscaledFile(sourceObjectKey: string, targetObjectKey: string): string | null { try { // Get upscaled paths for both source and target const { upscaledPath: sourcePath } = this.getWallpaperUpscaledInfo(sourceObjectKey); const { upscaledFilename: targetFilename } = this.getWallpaperUpscaledInfo(targetObjectKey); // If the source upscaled file doesn't exist, we can't copy if (!sourcePath) { log.error(`Cannot copy upscaled file: source ${sourcePath} does not exist`); return null; } // Create target path const targetPath = path.join(this.wallpaperDir, targetFilename); // Don't copy if it's the same file if (sourcePath === targetPath) { return sourcePath; } // Copy the file fs.copyFileSync(sourcePath, targetPath); log.info(`Copied upscaled file from ${sourcePath} to ${targetPath}`); return targetPath; } catch (error) { log.error(`Failed to copy upscaled file:`, error); return null; } } }