Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a68db6e
feat: add getCurrentRootFolder functionality
larryrider Feb 25, 2026
f959f48
feat: add getFolderByPath functionality
larryrider Feb 25, 2026
dd3d7d7
fix: improve error reporting
larryrider Feb 25, 2026
606040f
feat: add deleteByParentUuid method to FileRepository and integrate i…
larryrider Feb 25, 2026
24e7bd1
feat: implement generic getByPath functionality
larryrider Feb 25, 2026
7df17c7
refactor: move batch size management and generic folder path retrieval
larryrider Feb 26, 2026
1f5cb19
Merge branch 'feat/pb-5763-add-local-cache-database' into feat/add-lo…
larryrider Feb 26, 2026
1ac2a43
feat: add unit tests for DatabaseService configuration and integratio…
larryrider Feb 26, 2026
3d6dd98
feat: add maxRetries configuration to sdk apiSecurity
larryrider Feb 26, 2026
9682c59
deps: update fast-xml-parser dependency
larryrider Feb 26, 2026
e70e7fd
fix: add missing await
larryrider Feb 26, 2026
e1f18c0
feat: handle missing keys with optional parameter
larryrider Feb 26, 2026
a5b6032
feat: enhance folder content retrieval with subfolders and subfiles m…
larryrider Feb 26, 2026
4f39c71
feat: implement PathUtils class for file path data extraction
larryrider Feb 26, 2026
d4716d9
feat: remove undefined for type and fileId in DriveFile
larryrider Feb 26, 2026
ce4bca4
feat: add creationTime and modificationTime to newFolderItem and newD…
larryrider Feb 26, 2026
a122dcb
feat: add getByParentUuidNameAndType method
larryrider Feb 26, 2026
de10db1
feat: implement getByParentUuidAndName method and enhance getFileMeta…
larryrider Feb 26, 2026
17d1d6b
fix: add missing boolean to tests
larryrider Feb 26, 2026
59c9aad
tests: add default restoreMocks vitest property and fix tests
larryrider Feb 26, 2026
a6014c2
fix: refine path trimming logic in getFolderByPathGeneric method
larryrider Feb 26, 2026
09825e7
fix: sonarcloud maintainability issue
larryrider Feb 26, 2026
78271b2
fix: add missing awaits
larryrider Feb 27, 2026
5e26b87
fix: update workspace command descriptions to include WebDAV context
larryrider Feb 27, 2026
51d45cc
tests: add clearMocks default property
larryrider Feb 27, 2026
57c07d9
fix: replace void with await for thumbnail uploads
larryrider Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"dotenv": "17.3.1",
"express": "5.2.1",
"express-async-handler": "1.2.0",
"fast-xml-parser": "5.3.9",
"fast-xml-parser": "5.4.1",
"hash-wasm": "4.12.0",
"mime-types": "3.0.2",
"open": "11.0.0",
Expand All @@ -72,7 +72,7 @@
"@types/cli-progress": "3.11.6",
"@types/express": "5.0.6",
"@types/mime-types": "3.0.1",
"@types/node": "25.3.0",
"@types/node": "25.3.1",
"@types/range-parser": "1.2.7",
"@vitest/coverage-istanbul": "4.0.18",
"@vitest/spy": "4.0.18",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/create-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class CreateFolder extends Command {
const folderName = await this.getFolderName(flags['name'], nonInteractive);

const folderUuidFromFlag = await this.getFolderUuid(flags['id'], nonInteractive);
const folderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(folderUuidFromFlag, userCredentials);
const folderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(folderUuidFromFlag);

CLIUtils.doing('Creating folder...', flags['json']);
const [createNewFolder, requestCanceler] = await DriveFolderService.instance.createFolder({
Expand Down
2 changes: 1 addition & 1 deletion src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default class List extends Command {
if (!userCredentials) throw new MissingCredentialsError();

const folderUuidFromFlag = await this.getFolderUuid(flags['id'], nonInteractive);
const folderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(folderUuidFromFlag, userCredentials);
const folderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(folderUuidFromFlag);

const { folders, files } = await DriveFolderService.instance.getFolderContent(folderUuid);

Expand Down
5 changes: 1 addition & 4 deletions src/commands/move-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ export default class MoveFile extends Command {
nonInteractive,
reporter: this.log.bind(this),
});
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(
destinationFolderUuidFromFlag,
userCredentials,
);
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag);

const newFile = await DriveFileService.instance.moveFile(fileUuid, { destinationFolder: destinationFolderUuid });
const message = `File moved successfully to: ${destinationFolderUuid}`;
Expand Down
5 changes: 1 addition & 4 deletions src/commands/move-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ export default class MoveFolder extends Command {
nonInteractive,
reporter: this.log.bind(this),
});
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(
destinationFolderUuidFromFlag,
userCredentials,
);
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag);

const newFolder = await DriveFolderService.instance.moveFolder(folderUuid, {
destinationFolder: destinationFolderUuid,
Expand Down
5 changes: 1 addition & 4 deletions src/commands/trash-restore-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ export default class TrashRestoreFile extends Command {
nonInteractive,
reporter: this.log.bind(this),
});
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(
destinationFolderUuidFromFlag,
userCredentials,
);
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag);

const file = await DriveFileService.instance.moveFile(fileUuid, { destinationFolder: destinationFolderUuid });
const message = `File restored successfully to: ${destinationFolderUuid}`;
Expand Down
5 changes: 1 addition & 4 deletions src/commands/trash-restore-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ export default class TrashRestoreFolder extends Command {
nonInteractive,
reporter: this.log.bind(this),
});
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(
destinationFolderUuidFromFlag,
userCredentials,
);
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag);

const folder = await DriveFolderService.instance.moveFolder(folderUuid, {
destinationFolder: destinationFolderUuid,
Expand Down
7 changes: 2 additions & 5 deletions src/commands/upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ export default class UploadFile extends Command {
nonInteractive,
reporter: this.log.bind(this),
});
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(
destinationFolderUuidFromFlag,
userCredentials,
);
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag);

const timings = {
networkUpload: 0,
Expand Down Expand Up @@ -138,7 +135,7 @@ export default class UploadFile extends Command {

const thumbnailTimer = CLIUtils.timer();
if (fileSize > 0 && isThumbnailable && bufferStream) {
void ThumbnailService.instance.tryUploadThumbnail({
await ThumbnailService.instance.tryUploadThumbnail({
bufferStream,
fileType,
bucket,
Expand Down
5 changes: 1 addition & 4 deletions src/commands/upload-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ export default class UploadFolder extends Command {
nonInteractive: flags['non-interactive'],
reporter: this.log.bind(this),
});
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(
destinationFolderUuidFromFlag,
userCredentials,
);
const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag);

const progressBar = CLIUtils.progress(
{
Expand Down
2 changes: 1 addition & 1 deletion src/commands/workspaces-unset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class WorkspacesUnset extends Command {
static readonly args = {};
static readonly description =
'Unset the active workspace context for the current user session. ' +
'Once a workspace is unset, all subsequent commands (list, upload, download, etc.) ' +
'Once a workspace is unset, WebDAV and all of the subsequent CLI commands ' +
'will operate within the personal drive space until it is changed or set again.';
static readonly aliases = ['workspaces:unset'];
static readonly examples = ['<%= config.bin %> <%= command.id %>'];
Expand Down
4 changes: 2 additions & 2 deletions src/commands/workspaces-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class WorkspacesUse extends Command {
static readonly args = {};
static readonly description =
'Set the active workspace context for the current user session. ' +
'Once a workspace is selected, all subsequent commands (list, upload, download, etc.) ' +
'Once a workspace is selected, WebDAV and all of the subsequent CLI commands ' +
'will operate within that workspace until it is changed or unset.';
static readonly aliases = ['workspaces:use'];
static readonly examples = ['<%= config.bin %> <%= command.id %>'];
Expand Down Expand Up @@ -75,7 +75,7 @@ export default class WorkspacesUse extends Command {
});

const message =
`Workspace ${workspaceUuid} selected successfully. Now all drive commands (list, upload, download, etc.) ` +
`Workspace ${workspaceUuid} selected successfully. Now WebDAV and all of the CLI commands ` +
'will operate within this workspace until it is changed or unset.';
CLIUtils.success(this.log.bind(this), message);

Expand Down
8 changes: 8 additions & 0 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ export class AuthService {
return loginCreds.workspace;
};

public getCurrentRootFolder = async (): Promise<string> => {
const loginCreds = await ConfigService.instance.readUser();
if (!loginCreds?.token || !loginCreds?.user?.mnemonic) {
throw new MissingCredentialsError();
}
return loginCreds.workspace?.workspaceData?.workspaceUser?.rootFolderId ?? loginCreds.user.rootFolderId;
};

/**
* Logs the user out of the application by invoking the logout method
* from the authentication client. This will terminate the user's session
Expand Down
9 changes: 5 additions & 4 deletions src/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ export class ConfigService {
/**
* Gets the value from an environment key
* @param key The environment key to retrieve
* @throws {Error} If key is not found in process.env
* @param throwIfNotFound If true, throws an error if the key is not found
* @throws {Error} If key is not found in process.env and throwIfNotFound is true
* @returns The value from the environment variable
**/
public get = (key: keyof ConfigKeys): string => {
public get = (key: keyof ConfigKeys, throwIfNotFound = true): string => {
const value = process.env[key];
if (!value) throw new Error(`Config key ${key} was not found in process.env`);
return value;
if (!value && throwIfNotFound) throw new Error(`Config key ${key} was not found in process.env`);
return value ?? '';
};

/**
Expand Down
3 changes: 2 additions & 1 deletion src/services/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { DataSource } from 'typeorm';
import { DriveFileModel } from './drive-file/drive-file.model';
import { DriveFolderModel } from './drive-folder/drive-folder.model';
import { DRIVE_SQLITE_FILE } from '../../constants/configs';
import { ConfigService } from '../config.service';

export class DatabaseService {
public static readonly instance = new DatabaseService();

public dataSource = new DataSource(
process.env.NODE_ENV === 'test'
ConfigService.instance.get('NODE_ENV', false) === 'test'
? {
type: 'sqljs',
autoSave: false,
Expand Down
4 changes: 2 additions & 2 deletions src/services/database/drive-file/drive-file.attributes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export interface DriveFileAttributes {
uuid: string;
name: string;
type?: string | null;
fileId?: string | null;
type: string | null;
fileId: string | null;
folderUuid: string;
bucket: string;
createdAt: Date;
Expand Down
4 changes: 2 additions & 2 deletions src/services/database/drive-file/drive-file.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { DriveFileAttributes } from './drive-file.attributes';

export class DriveFile implements DriveFileAttributes {
name: string;
type?: string | null;
type: string | null;
uuid: string;
fileId?: string | null;
fileId: string | null;
folderUuid: string;
bucket: string;
createdAt: Date;
Expand Down
4 changes: 2 additions & 2 deletions src/services/database/drive-file/drive-file.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export class DriveFileModel implements DriveFileAttributes {
declare name: string;

@Column({ nullable: true, type: 'varchar' })
declare type?: string | null;
declare type: string | null;

@Column({ nullable: true, type: 'varchar' })
declare fileId?: string | null;
declare fileId: string | null;

@Column({ nullable: false, type: 'varchar' })
declare folderUuid: string;
Expand Down
43 changes: 34 additions & 9 deletions src/services/database/drive-file/drive-file.repository.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,67 @@
import { IsNull } from 'typeorm';
import { DatabaseUtils } from '../../../utils/database.utils';
import { ErrorUtils } from '../../../utils/errors.utils';
import { DatabaseService } from '../database.service';
import { DriveFile } from './drive-file.domain';
import { DriveFileModel } from './drive-file.model';

const BATCH_SIZE = 100;

export class FileRepository {
public static readonly instance = new FileRepository();

private fileRepository = DatabaseService.instance.dataSource.getRepository(DriveFileModel);
private readonly fileRepository = DatabaseService.instance.dataSource.getRepository(DriveFileModel);

public createOrUpdate = async (files: DriveFileModel[]) => {
public createOrUpdate = async (files: DriveFileModel[]): Promise<DriveFile[] | undefined> => {
try {
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const chunk = files.slice(i, i + BATCH_SIZE);
for (let i = 0; i < files.length; i += DatabaseUtils.CREATE_BATCH_SIZE) {
const chunk = files.slice(i, i + DatabaseUtils.CREATE_BATCH_SIZE);

await this.fileRepository.upsert(chunk, { conflictPaths: ['uuid'] });
}

return files.map((file) => DriveFile.build(file));
} catch (error) {
ErrorUtils.report(error, { files });
ErrorUtils.report(error, { createOrUpdate: files });
}
};

public updateByUuid = async (uuid: string, update: Partial<DriveFileModel>) => {
try {
return await this.fileRepository.update({ uuid }, update);
} catch (error) {
ErrorUtils.report(error, { uuid });
ErrorUtils.report(error, { updateByUuid: uuid });
}
};

public delete = async (uuids: string[]) => {
try {
return await this.fileRepository.delete(uuids);
} catch (error) {
ErrorUtils.report(error, { uuids });
ErrorUtils.report(error, { delete: uuids });
}
};

public deleteByParentUuid = async (parentUuid: string) => {
try {
return await this.fileRepository.delete({ folderUuid: parentUuid });
} catch (error) {
ErrorUtils.report(error, { deleteByParentUuid: parentUuid });
}
};

public getByParentUuidNameAndType = async (
parentUuid: string,
name: string,
type: string | null,
): Promise<DriveFile | undefined> => {
try {
const typeCondition = type ?? IsNull();
const file = await this.fileRepository.findOneBy({ folderUuid: parentUuid, name, type: typeCondition });
if (!file) {
return;
}
return DriveFile.build(file);
} catch (error) {
ErrorUtils.report(error, { getByParentuuidAndName: { parentUuid, name, type } });
}
};
}
42 changes: 37 additions & 5 deletions src/services/database/drive-folder/drive-folder.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { ErrorUtils } from '../../../utils/errors.utils';
import { DatabaseService } from '../database.service';
import { DriveFolder } from './drive-folder.domain';
import { DriveFolderModel } from './drive-folder.model';

const BATCH_SIZE = 100;
import { DatabaseUtils } from '../../../utils/database.utils';

export class FolderRepository {
public static readonly instance = new FolderRepository();

private folderRepository = DatabaseService.instance.dataSource.getRepository(DriveFolderModel);
private readonly folderRepository = DatabaseService.instance.dataSource.getRepository(DriveFolderModel);

public getByUuid = async (uuid: string): Promise<DriveFolder | undefined> => {
try {
Expand All @@ -34,12 +33,45 @@ export class FolderRepository {
}
};

public getByParentUuidAndName = async (parentUuid: string, name: string): Promise<DriveFolder | undefined> => {
try {
const folder = await this.folderRepository.findOneBy({ parentUuid, name });
if (!folder) {
return;
}
return DriveFolder.build(folder);
} catch (error) {
ErrorUtils.report(error, { getByParentuuidAndName: { parentUuid, name } });
}
};

public getByPath = async (path: string, parentUuid: string): Promise<DriveFolder | undefined> => {
try {
const onFound = async (uuid: string) => {
const folder = await this.folderRepository.findOneBy({ uuid });
if (!folder) {
return;
}
return DriveFolder.build(folder);
};

return DatabaseUtils.getFolderByPathGeneric({
path,
parentUuid,
onFound,
getByParentAndName: this.getByParentUuidAndName.bind(this),
});
} catch (error) {
ErrorUtils.report(error, { getByPath: path });
}
};

public createOrUpdate = async (files: DriveFolderModel[]) => {
if (files.length === 0) return;

try {
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const chunk = files.slice(i, i + BATCH_SIZE);
for (let i = 0; i < files.length; i += DatabaseUtils.CREATE_BATCH_SIZE) {
const chunk = files.slice(i, i + DatabaseUtils.CREATE_BATCH_SIZE);

await this.folderRepository.upsert(chunk, { conflictPaths: ['uuid'] });
}
Expand Down
Loading