Skip to content

Commit

Permalink
feat: only folderId is needed, default values are managed by the pkg (#2
Browse files Browse the repository at this point in the history
)

* feat: ✨ 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)
  • Loading branch information
totallynotdavid authored Oct 3, 2024
1 parent 80fc742 commit 4122b90
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 153 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
16 changes: 7 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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};
2 changes: 0 additions & 2 deletions src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down
188 changes: 155 additions & 33 deletions src/orchestrator.ts
Original file line number Diff line number Diff line change
@@ -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<DriveFileManagerConfig>;
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<void> {
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<void> {
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<void> {
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<void> {
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<DatabaseFile[]> {
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}`);
}
}

/**
Expand All @@ -76,17 +155,60 @@ export class DriveFileManager {
* @returns The local file path where the file was downloaded.
*/
async downloadFile(fileLink: string): Promise<string> {
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<boolean> {
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<void> {
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}`);
}
}
}
8 changes: 5 additions & 3 deletions src/services/authorizer.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -50,7 +52,7 @@ export async function saveCredentials(
}

export async function authorize(
config: DriveFileManagerConfig
config: InternalDriveFileManagerConfig
): Promise<Auth.OAuth2Client> {
const {tokenPath, credentialsPath} = config;
let client = await loadSavedCredentialsIfExist(tokenPath);
Expand Down
Loading

0 comments on commit 4122b90

Please sign in to comment.