Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ services:
WEBDAV_PASSWORD: "" # (Optional) Custom password for WebDAV authentication
WEBDAV_DELETE_FILES_PERMANENTLY: "" # (Optional) Delete files permanently. Set to 'true' to enable
WEBDAV_KEEPALIVE_ENABLED: "" # (Optional) Auto-renew the session/WebDAV server on failure. Defaults to 'true'. Set to 'false' to stop the container on failure instead
INXT_SQLITE_JOURNAL_MODE: "" # (Optional) Local cache journal mode. 'WAL' (default) or 'DELETE'. Use 'DELETE' if the CLI data dir is on a network share (NFS/SMB)
ports:
- "127.0.0.1:3005:3005" # Map container port to host. Change if WEBDAV_PORT is customized
```
Expand Down Expand Up @@ -54,6 +55,7 @@ docker run -d \
-e WEBDAV_PASSWORD="" \
-e WEBDAV_DELETE_FILES_PERMANENTLY="" \
-e WEBDAV_KEEPALIVE_ENABLED="" \
-e INXT_SQLITE_JOURNAL_MODE="" \
-p 127.0.0.1:3005:3005 \
internxt/webdav:latest
```
Expand Down
2 changes: 2 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ webdav_online() {
}


echo "[entrypoint] $(internxt --version 2>/dev/null || echo 'internxt version unknown')"

if [ -z "$INXT_USER" ] || [ -z "$INXT_PASSWORD" ]; then
echo "Error: INXT_USER and INXT_PASSWORD environment variables must be set."
exit 1
Expand Down
29 changes: 29 additions & 0 deletions src/services/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { DataSource } from 'typeorm';
import { DriveItemModel } from './drive-item/drive-item.model';
import { DRIVE_SQLITE_FILE } from '../../constants/configs';
import { ConfigService } from '../config.service';
import { logger } from '../../utils/logger.utils';

type DatabasePragmas = { pragma: (source: string) => unknown };
type JournalMode = 'WAL' | 'DELETE';

export class DatabaseService {
public static readonly instance = new DatabaseService();
Expand All @@ -21,9 +25,34 @@ export class DatabaseService {
logging: false,
synchronize: true,
entities: [DriveItemModel],
prepareDatabase: (db: DatabasePragmas) => DatabaseService.configureJournal(db),
},
);

public static readonly configureJournal = (db: DatabasePragmas) => {
const mode = DatabaseService.getJournalMode();
try {
const result = db.pragma(`journal_mode = ${mode}`) as { journal_mode?: string }[] | undefined;
const activeMode = result?.[0]?.journal_mode;
if (activeMode?.toUpperCase() !== mode) {
logger.warn(`Could not set SQLite journal mode to ${mode}, using ${activeMode ?? 'unknown'} instead`);
return;
}
if (mode === 'WAL') db.pragma('journal_size_limit = 16777216');
} catch (error) {
logger.warn(`Could not set SQLite journal mode to ${mode}: ${(error as Error).message}`);
}
};

private static readonly getJournalMode = (): JournalMode => {
const value = ConfigService.instance.get('INXT_SQLITE_JOURNAL_MODE', false).toUpperCase();
if (value === 'DELETE') return 'DELETE';
if (value && value !== 'WAL') {
logger.warn(`Unknown INXT_SQLITE_JOURNAL_MODE "${value}", expected WAL or DELETE. Using WAL`);
}
return 'WAL';
};

public initialize = () => {
if (!this.dataSource.isInitialized) {
return this.dataSource.initialize();
Expand Down
1 change: 1 addition & 0 deletions src/types/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface ConfigKeys {
readonly APP_MAGIC_SALT: string;
readonly NETWORK_URL: string;
readonly DESKTOP_HEADER: string;
readonly INXT_SQLITE_JOURNAL_MODE: string;
}
61 changes: 61 additions & 0 deletions test/services/database/database.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DatabaseService } from '../../../src/services/database/database.service
import { DriveItemModel } from '../../../src/services/database/drive-item/drive-item.model';
import { ConfigService } from '../../../src/services/config.service';
import { DRIVE_SQLITE_FILE } from '../../../src/constants/configs';
import { logger } from '../../../src/utils/logger.utils';

describe('Database Service', () => {
describe('dataSource configuration', () => {
Expand All @@ -28,10 +29,70 @@ describe('Database Service', () => {
expect(service.dataSource.options.database).toBe(DRIVE_SQLITE_FILE);
expect(service.dataSource.options.synchronize).toBe(true);
expect(service.dataSource.options.entities).toEqual([DriveItemModel]);
expect(service.dataSource.options).toHaveProperty('prepareDatabase');
expect(configServiceInstancespyOn).toHaveBeenCalledWith('NODE_ENV', false);
});
});

describe('configureJournal', () => {
const mockJournalMode = (value: string) => vi.spyOn(ConfigService.instance, 'get').mockReturnValueOnce(value);

test('when no journal mode is configured, then WAL mode and the journal size limit are set', () => {
mockJournalMode('');
const db = { pragma: vi.fn().mockReturnValueOnce([{ journal_mode: 'wal' }]) };

DatabaseService.configureJournal(db);

expect(db.pragma).toHaveBeenCalledWith('journal_mode = WAL');
expect(db.pragma).toHaveBeenCalledWith('journal_size_limit = 16777216');
});

test('when the journal mode is set to delete, then the rollback journal is used without a size limit', () => {
mockJournalMode('delete');
const db = { pragma: vi.fn().mockReturnValueOnce([{ journal_mode: 'delete' }]) };

DatabaseService.configureJournal(db);

expect(db.pragma).toHaveBeenCalledWith('journal_mode = DELETE');
expect(db.pragma).toHaveBeenCalledTimes(1);
});

test('when the journal mode is unknown, then a warning is logged and WAL mode is used', () => {
mockJournalMode('truncate');
const warnSpy = vi.spyOn(logger, 'warn').mockReturnValue(logger);
const db = { pragma: vi.fn().mockReturnValueOnce([{ journal_mode: 'wal' }]) };

DatabaseService.configureJournal(db);

expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('TRUNCATE'));
expect(db.pragma).toHaveBeenCalledWith('journal_mode = WAL');
});

test('when SQLite falls back to another journal mode, then a warning is logged and no size limit is set', () => {
mockJournalMode('');
const warnSpy = vi.spyOn(logger, 'warn').mockReturnValue(logger);
const db = { pragma: vi.fn().mockReturnValueOnce([{ journal_mode: 'memory' }]) };

DatabaseService.configureJournal(db);

expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('using memory instead'));
expect(db.pragma).toHaveBeenCalledTimes(1);
});

test('when setting the journal mode fails, then the error is logged and not thrown', () => {
mockJournalMode('');
const db = {
pragma: vi.fn().mockImplementation(() => {
throw new Error('database is locked');
}),
};
const warnSpy = vi.spyOn(logger, 'warn').mockReturnValue(logger);

expect(() => DatabaseService.configureJournal(db)).not.toThrow();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('database is locked'));
});
});

describe('Integration scenarios', () => {
test('when the database is initialized, cleared, and destroyed, then the sequence completes successfully', async () => {
const configServiceInstancespyOn = vi.spyOn(ConfigService.instance, 'get');
Expand Down
Loading