From 4122b9071be8f1643e97eccbcf963f0aef55a7e8 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 22:58:25 -0500 Subject: [PATCH] feat: only folderId is needed, default values are managed by the pkg (#2) * feat: :sparkles: automatically generate databasePath, downloadsPath, and logsPath using the provided folderId * chore(ignore): add txt to gitignore * fix: remove error types & reorder calls to make things faster * refactor: simplify ensureDirectoryExists function * only require folderId but allow users to customize * fix: extend type for authorizer * fix: path to json files (tokens are on separate folder) --- .gitignore | 3 +- src/config.ts | 16 ++- src/dev.ts | 2 - src/orchestrator.ts | 188 +++++++++++++++++++++++++++++------ src/services/authorizer.ts | 8 +- src/services/google-drive.ts | 87 ++++++++++++---- src/services/local-db.ts | 186 ++++++++++++++++++++++++---------- src/types/errors.ts | 20 ---- src/types/index.ts | 9 +- src/utils/index.ts | 13 +-- 10 files changed, 379 insertions(+), 153 deletions(-) delete mode 100644 src/types/errors.ts diff --git a/.gitignore b/.gitignore index 9f4395c..a4b6e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,13 @@ node_modules/ # Logs logs/ *.log +*.txt # Database storage/databases/*.sqlite # Auth tokens and credentials -storage/auth/*.json +storage/auth/**/*.json # Downloads storage/downloads/ diff --git a/src/config.ts b/src/config.ts index 9e4172c..056f3ec 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,17 +1,15 @@ import path from 'path'; +const baseDirectories = { + databases: path.join(process.cwd(), 'storage', 'databases'), + downloads: path.join(process.cwd(), 'storage', 'downloads'), + logs: path.join(process.cwd(), 'logs'), +}; + const defaultConfig = { folderId: '', tokenPath: path.join(process.cwd(), 'storage', 'auth', 'tokens', 'token.json'), credentialsPath: path.join(process.cwd(), 'storage', 'auth', 'credentials.json'), - databasePath: path.join( - process.cwd(), - 'storage', - 'databases', - 'drive_database.sqlite' - ), - downloadsPath: path.join(process.cwd(), 'storage', 'downloads'), - logsPath: path.join(process.cwd(), 'logs'), }; -export default defaultConfig; +export {defaultConfig, baseDirectories}; diff --git a/src/dev.ts b/src/dev.ts index 3715081..1f19bd7 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -10,8 +10,6 @@ import {DriveFileManager} from './index'; async function main() { const config = { - tokenPath: './storage/auth/token.json', - credentialsPath: './storage/auth/credentials.json', folderId: '1PZfURZ_iYmd2z7Ikn9ZYY7FY0nHOneSD', }; diff --git a/src/orchestrator.ts b/src/orchestrator.ts index 2b1290a..e27559b 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -1,73 +1,152 @@ +import path from 'path'; +import fs from 'fs/promises'; import {authorize} from '@/services/authorizer'; import {GoogleDriveService} from '@/services/google-drive'; import {FolderDatabase} from '@/services/local-db'; import {logger} from '@/utils/logger'; import {DriveFileManagerConfig, DatabaseFile} from '@/types'; -import defaultConfig from '@/config'; +import {defaultConfig, baseDirectories} from '@/config'; export class DriveFileManager { private googleDriveService!: GoogleDriveService; private folderDatabase!: FolderDatabase; private config: Required; + private initialized = false; constructor(config: DriveFileManagerConfig) { + const {folderId} = config; + + const databasePath = + config.databasePath ?? + path.join(baseDirectories.databases, `${folderId}_drive_database.sqlite`); + const downloadsPath = + config.downloadsPath ?? path.join(baseDirectories.downloads, folderId); + const logsPath = config.logsPath ?? path.join(baseDirectories.logs, folderId); + this.config = { ...defaultConfig, ...config, - tokenPath: config.tokenPath || defaultConfig.tokenPath, - credentialsPath: config.credentialsPath || defaultConfig.credentialsPath, - databasePath: config.databasePath || defaultConfig.databasePath, - downloadsPath: config.downloadsPath || defaultConfig.downloadsPath, - logsPath: config.logsPath || defaultConfig.logsPath, + tokenPath: config.tokenPath ?? defaultConfig.tokenPath, + credentialsPath: config.credentialsPath ?? defaultConfig.credentialsPath, + databasePath, + downloadsPath, + logsPath, }; + + this.initializeDirectories().catch(err => { + logger.error('Error initializing directories:', err); + }); + logger.setLogsPath(this.config.logsPath); } /** - * Initializes the orchestrator by authorizing and setting up services and database. + * Asynchronously creates necessary directories. + */ + private async initializeDirectories(): Promise { + const dirPromises = [ + fs.mkdir(path.dirname(this.config.databasePath), {recursive: true}), + fs.mkdir(this.config.downloadsPath, {recursive: true}), + fs.mkdir(this.config.logsPath, {recursive: true}), + ]; + + await Promise.all(dirPromises); + } + + /** + * Ensures the orchestrator is initialized before use. + */ + private async ensureInitialized(): Promise { + if (!this.initialized) { + await this.init(); + } + } + + /** + * Initializes the orchestrator by authorizing, validating the folder ID, + * and setting up services and the local database. */ async init(): Promise { + if (this.initialized) { + return; + } + try { logger.info('Initializing Orchestrator...'); - const authClient = await authorize({ - folderId: this.config.folderId, - tokenPath: this.config.tokenPath, - credentialsPath: this.config.credentialsPath, - databasePath: this.config.databasePath, - downloadsPath: this.config.downloadsPath, - logsPath: this.config.logsPath, - }); + const credentialsExist = await this.fileExists(this.config.credentialsPath); + if (!credentialsExist) { + console.error( + `\nCredentials file not found at "${this.config.credentialsPath}".` + ); + console.error( + 'Please obtain a credentials.json file by following the instructions at:' + ); + console.error( + 'https://developers.google.com/drive/api/v3/quickstart/nodejs\n' + ); + throw new Error('Credentials file not found.'); + } + + const [authClient] = await Promise.all([ + authorize({ + folderId: this.config.folderId, + tokenPath: this.config.tokenPath, + credentialsPath: this.config.credentialsPath, + }), + this.initializeDirectories(), + ]); this.googleDriveService = new GoogleDriveService( authClient, this.config.downloadsPath ); - this.folderDatabase = new FolderDatabase( - this.googleDriveService, - this.config.folderId, - this.config.databasePath, - logger - ); - await this.folderDatabase.initDatabase(); - - await this.folderDatabase.refresh(); + await Promise.all([ + this.googleDriveService.validateFolderId(this.config.folderId), + this.initializeDatabase(), + ]); + this.initialized = true; logger.info('Orchestrator initialized successfully.'); } catch (err) { - logger.error('Failed to initialize Orchestrator:', err); + logger.error('Error during initialization:', err); throw err; } } /** - * Searches for files in the database based on a query string. + * Initializes the folder database. + */ + private async initializeDatabase(): Promise { + this.folderDatabase = new FolderDatabase( + this.googleDriveService, + this.config.folderId, + this.config.databasePath, + logger + ); + + await this.folderDatabase.initDatabase(); + } + + /** + * Searches for files in the local database based on a query string. * @param query The search query. * @returns An array of DatabaseFile objects matching the query. */ async searchFiles(query: string): Promise { - return await this.folderDatabase.search(query); + await this.ensureInitialized(); + + try { + const results = await this.folderDatabase.search(query); + logger.info( + `Search completed. Found ${results.length} file(s) matching "${query}".` + ); + return results; + } catch (err) { + logger.error('Error searching files:', err); + throw new Error(`Failed to search files: ${(err as Error).message}`); + } } /** @@ -76,17 +155,60 @@ export class DriveFileManager { * @returns The local file path where the file was downloaded. */ async downloadFile(fileLink: string): Promise { - const fileExists = await this.folderDatabase.fileExists(fileLink); - if (!fileExists) { - throw new Error('File not found in the database.'); + await this.ensureInitialized(); + + try { + const cachedFilePath = await this.folderDatabase.getLocalFilePath(fileLink); + + if (cachedFilePath && (await this.fileExists(cachedFilePath))) { + logger.info(`File retrieved from cache at ${cachedFilePath}`); + return cachedFilePath; + } + + const fileExists = await this.folderDatabase.fileExists(fileLink); + if (!fileExists) { + throw new Error('File not found in the database.'); + } + + const localPath = + await this.googleDriveService.downloadFileFromGoogleDrive(fileLink); + logger.info(`File downloaded successfully to ${localPath}`); + + await this.folderDatabase.updateLocalFilePath(fileLink, localPath); + + return localPath; + } catch (err) { + logger.error('Error during file download:', err); + throw err; + } + } + + /** + * Checks if a file exists at the given path. + * @param filePath The path of the file to check. + * @returns True if the file exists, false otherwise. + */ + private async fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; } - return await this.googleDriveService.downloadFileFromGoogleDrive(fileLink); } /** - * Refreshes the database by fetching the latest files from Google Drive. + * Refreshes the local database by fetching the latest files from Google Drive. */ async refreshDatabase(): Promise { - await this.folderDatabase.refresh(); + await this.ensureInitialized(); + + try { + await this.folderDatabase.refresh(); + logger.info('Database refreshed successfully.'); + } catch (err) { + logger.error('Error refreshing the database:', err); + throw new Error(`Failed to refresh database: ${(err as Error).message}`); + } } } diff --git a/src/services/authorizer.ts b/src/services/authorizer.ts index 89de7eb..4452f1b 100644 --- a/src/services/authorizer.ts +++ b/src/services/authorizer.ts @@ -1,9 +1,10 @@ import fs from 'fs/promises'; +import path from 'path'; import {authenticate} from '@google-cloud/local-auth'; import {Auth, google} from 'googleapis'; import {ensureDirectoryExists} from '@/utils'; import {logger} from '@/utils/logger'; -import {DriveFileManagerConfig} from '@/types'; +import {InternalDriveFileManagerConfig} from '@/types'; export async function loadSavedCredentialsIfExist( tokenPath: string @@ -40,7 +41,8 @@ export async function saveCredentials( expiry_date: client.credentials.expiry_date, }); - await ensureDirectoryExists(tokenPath); + const tokenDir = path.dirname(tokenPath); + await ensureDirectoryExists(tokenDir); await fs.writeFile(tokenPath, payload); logger.info('Credentials saved successfully.'); } catch (err) { @@ -50,7 +52,7 @@ export async function saveCredentials( } export async function authorize( - config: DriveFileManagerConfig + config: InternalDriveFileManagerConfig ): Promise { const {tokenPath, credentialsPath} = config; let client = await loadSavedCredentialsIfExist(tokenPath); diff --git a/src/services/google-drive.ts b/src/services/google-drive.ts index 5908e91..1832c5a 100644 --- a/src/services/google-drive.ts +++ b/src/services/google-drive.ts @@ -4,7 +4,6 @@ import {google, drive_v3} from 'googleapis'; import {OAuth2Client} from 'google-auth-library'; import {logger} from '@/utils/logger'; import {GoogleFile} from '@/types'; -import {APIError, FileDownloadError} from '@/types/errors'; export class GoogleDriveService { private drive: drive_v3.Drive; @@ -15,6 +14,46 @@ export class GoogleDriveService { this.downloadsPath = downloadsPath; } + /** + * Validates that the provided folderId is valid, accessible, and represents a folder. + * @param folderId The ID of the folder to validate. + */ + public async validateFolderId(folderId: string): Promise { + try { + logger.info(`Validating folderId: ${folderId}`); + + const res = await this.drive.files.get({ + fileId: folderId, + fields: 'id, name, mimeType', + }); + + const file = res.data; + + if (!file) { + throw new Error(`Folder with ID ${folderId} not found.`); + } + + if (file.mimeType !== 'application/vnd.google-apps.folder') { + throw new Error(`The provided ID ${folderId} is not a folder.`); + } + + logger.info(`Validated folderId ${folderId}: ${file.name}`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + const errorCode = error.code; + const errorMessage = error.message || 'Unknown error occurred.'; + + if (errorCode === 404) { + throw new Error(`Folder with ID ${folderId} not found.`); + } else { + logger.error(`Error validating folderId ${folderId}:`, error); + throw new Error( + `An error occurred while validating folderId ${folderId}: ${errorMessage}` + ); + } + } + } + /** * Fetches all folders from Google Drive. * @returns A Map of folder IDs to folder metadata. @@ -43,7 +82,7 @@ export class GoogleDriveService { return new Map(folders.map(folder => [folder.id!, folder])); } catch (err) { logger.error('Error fetching folders:', err); - throw new APIError(`Failed to fetch folders: ${(err as Error).message}`); + throw new Error(`Failed to fetch folders: ${(err as Error).message}`); } } @@ -72,9 +111,14 @@ export class GoogleDriveService { } }; - rootFolderIds.forEach(id => processFolder(id)); - logger.info(`Built folder tree with ${allFolderIds.size} folders.`); - return Array.from(allFolderIds); + try { + rootFolderIds.forEach(id => processFolder(id)); + logger.info(`Built folder tree with ${allFolderIds.size} folders.`); + return Array.from(allFolderIds); + } catch (err) { + logger.error('Error building folder tree:', err); + throw new Error(`Failed to build folder tree: ${(err as Error).message}`); + } } /** @@ -101,7 +145,9 @@ export class GoogleDriveService { do { let queryString = `(${parentQueries}) and mimeType != 'application/vnd.google-apps.folder' and trashed = false`; if (query.trim() !== '') { - queryString += ` and name contains '${query}'`; + // Escape single quotes in query + const sanitizedQuery = query.replace(/'/g, "\\'"); + queryString += ` and name contains '${sanitizedQuery}'`; } const res = await this.drive.files.list({ @@ -125,11 +171,11 @@ export class GoogleDriveService { } while (pageToken); } - logger.info(`Found ${files.length} files.`); + logger.info(`Found ${files.length} file(s) matching the criteria.`); return files; } catch (err) { logger.error('Error searching files:', err); - throw new APIError(`Failed to search files: ${(err as Error).message}`); + throw new Error(`Failed to search files: ${(err as Error).message}`); } } @@ -143,10 +189,15 @@ export class GoogleDriveService { folderIds: string[]; files: GoogleFile[]; }> { - const folderMap = await this.getAllFolders(); - const folderIds = await this.buildFolderTree(folderMap, rootFolderIds); - const files = await this.searchFilesInFolders(folderIds); - return {folderMap, folderIds, files}; + try { + const folderMap = await this.getAllFolders(); + const folderIds = await this.buildFolderTree(folderMap, rootFolderIds); + const files = await this.searchFilesInFolders(folderIds); + return {folderMap, folderIds, files}; + } catch (err) { + logger.error('Error fetching all files:', err); + throw new Error(`Failed to fetch all files: ${(err as Error).message}`); + } } /** @@ -175,19 +226,17 @@ export class GoogleDriveService { logger.info(`Downloaded file to ${filePath}`); resolve(); }) - .on('error', err => { + .on('error', (err: Error) => { logger.error('Error downloading file:', err); - reject(new FileDownloadError('Failed to download the file.')); + reject(new Error('Failed to download the file.')); }) .pipe(dest); }); return filePath; - } catch (err) { - logger.error('Error in downloadFileFromGoogleDrive:', err); - throw new FileDownloadError( - `Failed to download file: ${(err as Error).message}` - ); + } catch (err: unknown) { + logger.error('Error downloading file:', err); + throw new Error('Failed to download the file.'); } } diff --git a/src/services/local-db.ts b/src/services/local-db.ts index 86d02b5..9a2ae8a 100644 --- a/src/services/local-db.ts +++ b/src/services/local-db.ts @@ -4,7 +4,6 @@ import {Logger} from '@/utils/logger'; import {GoogleDriveService} from '@/services/google-drive'; import {escapeSingleQuotes} from '@/utils'; import {GoogleFile, RefreshResult, DatabaseFile} from '@/types'; -import {DatabaseError} from '@/types/errors'; type SQLiteDB = Database; type SQLiteStmt = Statement; @@ -16,6 +15,13 @@ export class FolderDatabase { private databasePath: string; private logger: Logger; + // We are caching prepared statements + private insertOrUpdateStmt!: SQLiteStmt; + private selectLocalPathStmt!: SQLiteStmt; + private updateLocalPathStmt!: SQLiteStmt; + private checkFileExistsStmt!: SQLiteStmt; + private searchStmt!: SQLiteStmt; + constructor( googleDriveService: GoogleDriveService, folderId: string, @@ -29,7 +35,8 @@ export class FolderDatabase { } /** - * Initializes the SQLite database and creates necessary tables and indexes. + * Initializes the SQLite database, creates necessary tables, indexes, + * and prepares frequently used statements. */ async initDatabase(): Promise { try { @@ -43,7 +50,8 @@ export class FolderDatabase { id TEXT PRIMARY KEY, name TEXT NOT NULL, parents TEXT, - webViewLink TEXT NOT NULL + webViewLink TEXT NOT NULL, + localPath TEXT ); `); @@ -52,9 +60,63 @@ export class FolderDatabase { `); this.logger.info('SQLite database initialized successfully.'); + + await this.prepareStatements(); } catch (err) { this.logger.error('Error initializing SQLite database:', err); - throw new DatabaseError('Failed to initialize the database.'); + throw new Error('Failed to initialize the database.'); + } + } + + /** + * Prepares and caches frequently used SQL statements to enhance performance. + */ + private async prepareStatements(): Promise { + try { + this.insertOrUpdateStmt = await this.db.prepare(` + INSERT INTO files (id, name, parents, webViewLink, localPath) + VALUES (?, ?, ?, ?, COALESCE((SELECT localPath FROM files WHERE id = ?), NULL)) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + parents = excluded.parents, + webViewLink = excluded.webViewLink; + `); + + this.selectLocalPathStmt = await this.db.prepare(` + SELECT localPath FROM files WHERE id = ?; + `); + + this.updateLocalPathStmt = await this.db.prepare(` + UPDATE files SET localPath = ? WHERE id = ?; + `); + + this.checkFileExistsStmt = await this.db.prepare(` + SELECT 1 FROM files WHERE id = ? LIMIT 1; + `); + + this.searchStmt = await this.db.prepare(` + SELECT * FROM files WHERE name LIKE ?; + `); + } catch (err) { + this.logger.error('Error preparing SQL statements:', err); + throw new Error('Failed to prepare SQL statements.'); + } + } + + /** + * Closes all prepared statements and the database connection gracefully. + */ + async closeDatabase(): Promise { + try { + await this.insertOrUpdateStmt.finalize(); + await this.selectLocalPathStmt.finalize(); + await this.updateLocalPathStmt.finalize(); + await this.checkFileExistsStmt.finalize(); + await this.searchStmt.finalize(); + await this.db.close(); + this.logger.info('SQLite database closed successfully.'); + } catch (err) { + this.logger.error('Error closing SQLite database:', err); } } @@ -64,57 +126,44 @@ export class FolderDatabase { */ async getExistingFileIds(): Promise> { try { - const rows: DatabaseFile[] = await this.db.all(`SELECT id FROM files;`); + const rows: Pick[] = + await this.db.all(`SELECT id FROM files;`); return new Set(rows.map(row => row.id)); } catch (err) { this.logger.error('Error fetching existing file IDs:', err); - throw new DatabaseError('Failed to fetch existing file IDs.'); + throw new Error('Failed to fetch existing file IDs.'); } } /** - * Updates the database with new and existing files. + * Updates the database with new and existing files in bulk within a transaction. * @param files Array of GoogleFile objects to be inserted or updated. */ async updateDatabase(files: GoogleFile[]): Promise { - const insertStmt = ` - INSERT INTO files (id, name, parents, webViewLink) - VALUES (?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET - name = excluded.name, - parents = excluded.parents, - webViewLink = excluded.webViewLink; - `; - - let update: SQLiteStmt; - try { - update = await this.db.prepare(insertStmt); - } catch (err) { - this.logger.error('Error preparing INSERT statement:', err); - throw new DatabaseError('Failed to prepare database statement.'); - } - try { await this.db.run('BEGIN TRANSACTION;'); for (const file of files) { const parentsStr = file.parents ? JSON.stringify(file.parents) : null; - await update.run(file.id, file.name, parentsStr, file.webViewLink); + await this.insertOrUpdateStmt.run( + file.id, + file.name, + parentsStr, + file.webViewLink, + file.id + ); } await this.db.run('COMMIT;'); this.logger.info('Database updated successfully.'); } catch (err) { await this.db.run('ROLLBACK;'); this.logger.error('Error during database update:', err); - throw new DatabaseError( - `Failed to update database: ${(err as Error).message}` - ); - } finally { - await update.finalize(); + throw new Error(`Failed to update database: ${(err as Error).message}`); } } /** * Deletes files from the database that are no longer present in Google Drive. + * Executes the deletion in a single statement using NOT IN with formatted placeholders. * @param currentFileIds Set of currently fetched file IDs. */ async deleteRemovedFiles(currentFileIds: Set): Promise { @@ -138,14 +187,13 @@ export class FolderDatabase { ); } catch (err) { this.logger.error('Error deleting removed files:', err); - throw new DatabaseError( - `Failed to delete removed files: ${(err as Error).message}` - ); + throw new Error(`Failed to delete removed files: ${(err as Error).message}`); } } /** * Refreshes the database by fetching the latest files from Google Drive. + * Optimized to reduce redundant operations and log meaningful information. * @returns RefreshResult containing total and new file counts. */ async refresh(): Promise { @@ -171,20 +219,14 @@ export class FolderDatabase { return {totalFiles, newFiles}; } catch (error: unknown) { - if (error instanceof Error) { - this.logger.error('Failed to refresh database:', error.message); - throw new DatabaseError(`Failed to refresh database: ${error.message}`); - } else { - this.logger.error('Failed to refresh database due to an unknown error.'); - throw new DatabaseError( - 'Failed to refresh database due to an unknown error.' - ); - } + this.logger.error('Error refreshing the database:', error); + throw new Error('Failed to refresh the database.'); } } /** * Searches for files in the database based on a query string. + * Utilizes a prepared statement for efficiency. * @param query The search query. * @returns An array of DatabaseFile objects matching the query. */ @@ -193,23 +235,21 @@ export class FolderDatabase { const queryString = `%${escapedQuery}%`; try { - const results: DatabaseFile[] = await this.db.all( - `SELECT * FROM files WHERE name LIKE ?;`, - [queryString] - ); + const rows: DatabaseFile[] = await this.searchStmt.all([queryString]); - return results.map(file => ({ + return rows.map(file => ({ ...file, parents: file.parents ? JSON.parse(file.parents) : null, })); } catch (err) { this.logger.error('Error during search query:', err); - throw new DatabaseError(`Search query failed: ${(err as Error).message}`); + throw new Error(`Search query failed: ${(err as Error).message}`); } } /** * Checks if a file exists in the database based on its webViewLink. + * Utilizes a prepared statement for efficiency. * @param fileLink The webViewLink of the file. * @returns Boolean indicating existence. */ @@ -218,14 +258,54 @@ export class FolderDatabase { if (!fileId) return false; try { - const result = await this.db.get(`SELECT id FROM files WHERE id = ?;`, [ - fileId, - ]); + const result = await this.checkFileExistsStmt.get([fileId]); return !!result; } catch (err) { this.logger.error('Error checking file existence:', err); - throw new DatabaseError( - `Failed to check file existence: ${(err as Error).message}` + throw new Error(`Failed to check file existence: ${(err as Error).message}`); + } + } + + /** + * Retrieves the local file path for a file based on its webViewLink. + * Utilizes a prepared statement for efficiency. + * @param fileLink The webViewLink of the file. + * @returns The local file path if it exists, otherwise null. + */ + async getLocalFilePath(fileLink: string): Promise { + const fileId = this.googleDriveService.extractFileIdFromLink(fileLink); + if (!fileId) return null; + + try { + const result = await this.selectLocalPathStmt.get([fileId]); + return result?.localPath || null; + } catch (err) { + this.logger.error('Error retrieving local file path:', err); + throw new Error( + `Failed to retrieve local file path: ${(err as Error).message}` + ); + } + } + + /** + * Updates the local file path for a given file based on its webViewLink. + * Utilizes a prepared statement for efficiency. + * @param fileLink The webViewLink of the file. + * @param localPath The local path where the file is stored. + */ + async updateLocalFilePath(fileLink: string, localPath: string): Promise { + const fileId = this.googleDriveService.extractFileIdFromLink(fileLink); + if (!fileId) { + throw new Error('Invalid file link provided.'); + } + + try { + await this.updateLocalPathStmt.run([localPath, fileId]); + this.logger.info(`Updated local file path for file ID ${fileId}.`); + } catch (err) { + this.logger.error('Error updating local file path:', err); + throw new Error( + `Failed to update local file path: ${(err as Error).message}` ); } } diff --git a/src/types/errors.ts b/src/types/errors.ts deleted file mode 100644 index 0dceb86..0000000 --- a/src/types/errors.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class DatabaseError extends Error { - constructor(message: string) { - super(message); - this.name = 'DatabaseError'; - } -} - -export class APIError extends Error { - constructor(message: string) { - super(message); - this.name = 'APIError'; - } -} - -export class FileDownloadError extends Error { - constructor(message: string) { - super(message); - this.name = 'FileDownloadError'; - } -} diff --git a/src/types/index.ts b/src/types/index.ts index 23ce0c7..bd86faf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,13 +5,18 @@ export interface AuthClientConfig { export interface DriveFileManagerConfig { folderId: string; - tokenPath: string; - credentialsPath: string; + tokenPath?: string; + credentialsPath?: string; databasePath?: string; downloadsPath?: string; logsPath?: string; } +export interface InternalDriveFileManagerConfig extends DriveFileManagerConfig { + tokenPath: string; + credentialsPath: string; +} + export interface GoogleFile { id: string; name: string; diff --git a/src/utils/index.ts b/src/utils/index.ts index 961b91b..fd344d7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -10,16 +10,7 @@ export function escapeSingleQuotes(input: string): string { return input.replace(/'/g, "''"); } -export async function ensureDirectoryExists(filePath: string) { +export async function ensureDirectoryExists(filePath: string): Promise { const dirname = path.dirname(filePath); - try { - await fs.access(dirname); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - if (err.code === 'ENOENT') { - await fs.mkdir(dirname, {recursive: true}); - } else { - throw err; - } - } + await fs.mkdir(dirname, {recursive: true}); }