backsplash-app
Version:
An AI powered wallpaper app.
304 lines (261 loc) • 9.8 kB
text/typescript
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;
}
}
}