Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0b19169
deps: add better-sqlite3 dependency
larryrider Jan 21, 2026
9be5eb3
deps: update dependencies
larryrider Jan 22, 2026
9e31270
refactor: update DRIVE_SQLITE_FILE path to use .db extension and remo…
larryrider Jan 23, 2026
9c30d8d
refactor: remove unused attributes from DriveFile and DriveFolder models
larryrider Jan 30, 2026
fd5b2d4
deps: add typeorm dependency to package.json
larryrider Feb 5, 2026
599b122
feat: add DatabaseService for local cache database initialization
larryrider Feb 13, 2026
fc90a46
fix: entity name in DriveFolderModel
larryrider Feb 13, 2026
dfcb2e7
feat: init database repositories
larryrider Feb 18, 2026
eec88ec
feat: init database service
larryrider Feb 18, 2026
31abce2
fix: nullable parentUuid
larryrider Feb 18, 2026
15830bd
feat: add creationTime and modificationTime
larryrider Feb 19, 2026
d180c7c
deps: add sql.js dependency
larryrider Feb 19, 2026
176cdc4
feat: support nullable type and fileId, implement updateByUuid
larryrider Feb 20, 2026
214f968
feat: add database clear on webdav start
larryrider Feb 20, 2026
b1fe2d5
Merge branch 'main' into feat/pb-5763-add-local-cache-database
larryrider Feb 20, 2026
c11260a
fix: nullable fileId and type
larryrider Feb 20, 2026
e2280f4
feat: update dependencies
larryrider Feb 25, 2026
309bfcd
feat: add methods for fetching folders by UUID and parent UUID
larryrider Feb 25, 2026
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
904210a
feat: remove condition from createOrUpdate
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
2aa287f
Merge pull request #508 from internxt/feat/add-local-find-by-path
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
21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,22 @@
],
"dependencies": {
"@dashlane/pqc-kem-kyber512-node": "1.0.0",
"@inquirer/prompts": "8.2.0",
"@internxt/inxt-js": "2.2.9",
"@inquirer/prompts": "8.3.0",
"@internxt/inxt-js": "2.3.0",
"@internxt/lib": "1.4.1",
"@internxt/sdk": "1.13.1",
"@oclif/core": "4.8.0",
"@internxt/sdk": "1.15.0",
"@oclif/core": "4.8.1",
"@oclif/plugin-autocomplete": "3.2.40",
"axios": "1.13.5",
"better-sqlite3": "12.6.2",
"bip39": "3.1.0",
"body-parser": "2.2.2",
"cli-progress": "3.12.0",
"dayjs": "1.11.19",
"dotenv": "17.3.1",
"express": "5.2.1",
"express-async-handler": "1.2.0",
"fast-xml-parser": "5.3.6",
"fast-xml-parser": "5.4.1",
"hash-wasm": "4.12.0",
"mime-types": "3.0.2",
"open": "11.0.0",
Expand All @@ -61,6 +62,7 @@
"range-parser": "1.2.1",
"selfsigned": "5.5.0",
"tty-table": "5.0.0",
"typeorm": "0.3.28",
"winston": "3.19.0"
},
"devDependencies": {
Expand All @@ -70,17 +72,18 @@
"@types/cli-progress": "3.11.6",
"@types/express": "5.0.6",
"@types/mime-types": "3.0.1",
"@types/node": "25.2.3",
"@types/node": "25.3.1",
"@types/range-parser": "1.2.7",
"@vitest/coverage-istanbul": "4.0.18",
"@vitest/spy": "4.0.18",
"eslint": "9.39.2",
"husky": "9.1.7",
"lint-staged": "16.2.7",
"nodemon": "3.1.11",
"oclif": "4.22.77",
"nodemon": "3.1.14",
"oclif": "4.22.81",
"prettier": "3.8.1",
"rimraf": "6.1.2",
"rimraf": "6.1.3",
"sql.js": "1.14.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vitest": "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
3 changes: 1 addition & 2 deletions src/constants/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import os from 'node:os';

export const INTERNXT_CLI_DATA_DIR = path.join(os.homedir(), '.internxt-cli');
export const INTERNXT_CLI_LOGS_DIR = path.join(INTERNXT_CLI_DATA_DIR, 'logs');
export const INTERNXT_TMP_DIR = os.tmpdir();
export const CREDENTIALS_FILE = path.join(INTERNXT_CLI_DATA_DIR, '.inxtcli');
export const DRIVE_SQLITE_FILE = path.join(INTERNXT_CLI_DATA_DIR, 'internxt-cli-drive.sqlite');
export const DRIVE_SQLITE_FILE = path.join(INTERNXT_CLI_DATA_DIR, 'internxt-cli-drive.db');
export const WEBDAV_SSL_CERTS_DIR = path.join(INTERNXT_CLI_DATA_DIR, 'certs');
export const WEBDAV_CONFIGS_FILE = path.join(INTERNXT_CLI_DATA_DIR, 'config.webdav.inxt');
export const WEBDAV_DEFAULT_HOST = '127.0.0.1';
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/prerun/auth_check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SdkManager } from '../../services/sdk-manager.service';
import { AuthService } from '../../services/auth.service';
import Webdav from '../../commands/webdav';
import WebDAVConfig from '../../commands/webdav-config';
import { DatabaseService } from '../../services/database/database.service';

const CommandsToSkip = [Whoami, Login, LoginLegacy, Logout, Logs, Webdav, WebDAVConfig];
const hook: Hook<'prerun'> = async function (opts) {
Expand All @@ -22,6 +23,7 @@ const hook: Hook<'prerun'> = async function (opts) {
SdkManager.init({ token, workspaceToken: workspace?.workspaceCredentials.token });
CLIUtils.done(jsonFlag);
CLIUtils.clearPreviousLine(jsonFlag);
await DatabaseService.instance.initialize();
} catch (error) {
const err = error as Error;
CLIUtils.catchError({
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
43 changes: 43 additions & 0 deletions src/services/database/database.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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(
ConfigService.instance.get('NODE_ENV', false) === 'test'
? {
type: 'sqljs',
autoSave: false,
logging: false,
synchronize: true,
entities: [DriveFileModel, DriveFolderModel],
}
: {
type: 'better-sqlite3',
database: DRIVE_SQLITE_FILE,
logging: false,
synchronize: true,
entities: [DriveFileModel, DriveFolderModel],
},
);

public initialize = () => {
return this.dataSource.initialize();
};

public destroy = () => {
return this.dataSource.destroy();
};

public clear = () => {
return this.dataSource.synchronize(true);
};

public drop = () => {
return this.dataSource.dropDatabase();
};
}
9 changes: 3 additions & 6 deletions src/services/database/drive-file/drive-file.attributes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
export interface DriveFileAttributes {
id: number;
name: string;
type?: string;
uuid: string;
fileId: string;
folderId: number;
name: string;
type: string | null;
fileId: string | null;
folderUuid: string;
bucket: string;
relativePath: string;
createdAt: Date;
updatedAt: Date;
size: number;
Expand Down
18 changes: 2 additions & 16 deletions src/services/database/drive-file/drive-file.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { DriveFileItem } from '../../../types/drive.types';
import { DriveFileAttributes } from './drive-file.attributes';

export class DriveFile implements DriveFileAttributes {
id: number;
name: string;
type?: string;
type: string | null;
uuid: string;
fileId: string;
folderId: number;
fileId: string | null;
folderUuid: string;
bucket: string;
relativePath: string;
createdAt: Date;
updatedAt: Date;
size: number;
Expand All @@ -19,31 +16,25 @@ export class DriveFile implements DriveFileAttributes {
modificationTime: Date;

constructor({
id,
name,
type,
uuid,
fileId,
folderId,
folderUuid,
bucket,
relativePath,
createdAt,
updatedAt,
size,
status,
creationTime,
modificationTime,
}: DriveFileAttributes) {
this.id = id;
this.name = name;
this.type = type;
this.uuid = uuid;
this.fileId = fileId;
this.folderId = folderId;
this.folderUuid = folderUuid;
this.bucket = bucket;
this.relativePath = relativePath;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.size = size;
Expand All @@ -58,15 +49,12 @@ export class DriveFile implements DriveFileAttributes {

public toJSON(): DriveFileAttributes {
return {
id: this.id,
name: this.name,
type: this.type,
uuid: this.uuid,
fileId: this.fileId,
folderId: this.folderId,
folderUuid: this.folderUuid,
bucket: this.bucket,
relativePath: this.relativePath,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
size: this.size,
Expand All @@ -79,12 +67,10 @@ export class DriveFile implements DriveFileAttributes {
public toItem(): DriveFileItem {
return {
itemType: 'file',
id: this.id,
name: this.name,
type: this.type,
uuid: this.uuid,
fileId: this.fileId,
folderId: this.folderId,
folderUuid: this.folderUuid,
bucket: this.bucket,
createdAt: this.createdAt,
Expand Down
Loading