Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: only folderId is needed, default values are managed by the pkg #2

Merged
merged 7 commits into from
Oct 3, 2024
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
Loading