Files
myDrive/backend/db/sqliteDB/fileDB.ts
T
oxmc ecc340e7b1
Release Please / release-please (push) Has been cancelled
Docker Build and Push (Development) / build-and-push-dev (push) Has been cancelled
feat: add dark mode UI and SQLite as alternate database backend
Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage,
with a theme toggle in the Header and Settings page. Foundation didn't exist
yet despite prior assumption, so it was built from scratch.

SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at
runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail
bytes continue to use the existing fs/S3 storage path. Implemented via a shared
DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations,
selected through dbFactory.ts, with JWT/encryption logic extracted into
userCrypto.ts so both engines share it.
2026-07-05 04:18:33 -07:00

453 lines
12 KiB
TypeScript

import db from "../connections/sqlite";
import { ObjectId } from "mongodb";
import { IFileDB } from "../dbTypes";
import { FileListQueryType } from "../../types/file-types";
const rowToFile = (row: any) => {
if (!row) return null;
return {
_id: row.id,
length: row.length,
chunkSize: row.chunkSize,
uploadDate: row.uploadDate,
filename: row.filename,
metadata: {
owner: row.owner,
parent: row.parent,
parentList: row.parentList,
hasThumbnail: !!row.hasThumbnail,
isVideo: !!row.isVideo,
thumbnailID: row.thumbnailID || undefined,
size: row.size,
IV: row.iv,
linkType: row.linkType || undefined,
link: row.link || undefined,
filePath: row.filePath || undefined,
s3ID: row.s3ID || undefined,
personalFile: row.personalFile ? true : undefined,
trashed: row.trashed ? true : null,
processingFile: row.processingFile ? true : null,
},
};
};
const sortColumn = (sortBy: string): { column: string; direction: string } => {
if (sortBy === "date_desc") return { column: "uploadDate", direction: "DESC" };
if (sortBy === "date_asc") return { column: "uploadDate", direction: "ASC" };
if (sortBy === "alp_desc")
return { column: "filename COLLATE NOCASE", direction: "DESC" };
return { column: "filename COLLATE NOCASE", direction: "ASC" };
};
class FileDB implements IFileDB {
constructor() {}
// READ
getPublicFile = async (fileID: string) => {
const row = db.prepare("SELECT * FROM files WHERE id = ?").get(fileID);
return rowToFile(row);
};
getPublicInfo = async (fileID: string, tempToken: string) => {
const row = db
.prepare("SELECT * FROM files WHERE id = ? AND link = ?")
.get(fileID, tempToken);
return rowToFile(row);
};
getFileInfo = async (fileID: string, userID: string) => {
const row = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
return rowToFile(row);
};
getQuickList = async (userID: string, limit: number) => {
const rows = db
.prepare(
`SELECT * FROM files WHERE owner = ? AND trashed = 0 AND processingFile = 0
ORDER BY uploadDate DESC LIMIT ?`
)
.all(userID, limit);
return rows.map(rowToFile);
};
getList = async (
queryData: FileListQueryType,
sortBy: string,
limit: number
) => {
const {
userID,
search,
parent,
startAtDate,
startAtName,
trashMode,
mediaMode,
mediaFilter,
} = queryData;
const conditions: string[] = ["owner = ?"];
const params: any[] = [userID];
if (search && search !== "") {
conditions.push("filename LIKE ? COLLATE NOCASE");
params.push(`%${search}%`);
} else if (!mediaMode) {
conditions.push("parent = ?");
params.push(parent);
}
if (sortBy === "date_desc" && startAtDate) {
conditions.push("uploadDate < ?");
params.push(new Date(startAtDate).toISOString());
} else if (sortBy === "date_asc" && startAtDate) {
conditions.push("uploadDate > ?");
params.push(new Date(startAtDate).toISOString());
} else if (sortBy === "alp_desc" && startAtName) {
conditions.push("filename < ? COLLATE NOCASE");
params.push(startAtName);
} else if (sortBy === "alp_asc" && startAtName) {
conditions.push("filename > ? COLLATE NOCASE");
params.push(startAtName);
}
conditions.push("trashed = ?");
params.push(trashMode ? 1 : 0);
if (mediaMode) {
conditions.push("hasThumbnail = 1");
if (mediaFilter === "photos") conditions.push("isVideo = 0");
else if (mediaFilter === "videos") conditions.push("isVideo = 1");
}
conditions.push("processingFile = 0");
const { column, direction } = sortColumn(sortBy);
const rows = db
.prepare(
`SELECT * FROM files WHERE ${conditions.join(
" AND "
)} ORDER BY ${column} ${direction} LIMIT ?`
)
.all(...params, limit);
return rows.map(rowToFile);
};
getFileSearchList = async (
userID: string,
searchQuery: RegExp | string,
trashMode: boolean,
mediaMode: boolean
) => {
const search =
searchQuery instanceof RegExp ? searchQuery.source : searchQuery;
const conditions = [
"owner = ?",
"filename LIKE ? COLLATE NOCASE",
"trashed = ?",
"processingFile = 0",
];
const params: any[] = [userID, `%${search}%`, trashMode ? 1 : 0];
if (mediaMode) conditions.push("hasThumbnail = 1");
const rows = db
.prepare(
`SELECT * FROM files WHERE ${conditions.join(" AND ")} LIMIT 10`
)
.all(...params);
return rows.map(rowToFile);
};
getFileListByIncludedParent = async (
userID: string,
parentListString: string
) => {
const rows = db
.prepare(
"SELECT * FROM files WHERE owner = ? AND parentList LIKE ?"
)
.all(userID, `%${parentListString}%`);
return rows.map(rowToFile);
};
getFileListByOwner = async (userID: string) => {
const rows = db.prepare("SELECT * FROM files WHERE owner = ?").all(userID);
return rows.map(rowToFile);
};
getFileListByParent = async (userID: string, parent: string) => {
const rows = db
.prepare("SELECT * FROM files WHERE owner = ? AND parent = ?")
.all(userID, parent);
return rows.map(rowToFile);
};
// UPDATE
updateFileUploadedFile = async (
fileID: string,
userID: string,
parent: string,
parentList: string
) => {
db.prepare(
`UPDATE files SET parent = ?, parentList = ?, processingFile = 0
WHERE id = ? AND owner = ?`
).run(parent, parentList, fileID, userID);
return this.getFileInfo(fileID, userID);
};
updateFolderUploadedFile = async (
fileID: string,
userID: string,
parent: string,
parentList: string
) => {
return this.updateFileUploadedFile(fileID, userID, parent, parentList);
};
setThumbnail = async (fileID: string, thumbnailID: string) => {
const result = db
.prepare(
`UPDATE files SET hasThumbnail = 1, thumbnailID = ?
WHERE id = ? AND hasThumbnail = 0`
)
.run(thumbnailID, fileID);
if (result.changes === 0) return null;
const row = db.prepare("SELECT * FROM files WHERE id = ?").get(fileID);
return rowToFile(row);
};
setVideoThumbnail = async (
fileID: string,
userID: string,
thumbnailID: string
) => {
const result = db
.prepare(
`UPDATE files SET hasThumbnail = 1, thumbnailID = ?, isVideo = 1
WHERE id = ? AND owner = ?`
)
.run(thumbnailID, fileID, userID);
if (result.changes === 0) return null;
return this.getFileInfo(fileID, userID);
};
removeOneTimePublicLink = async (fileID: string) => {
db.prepare(
"UPDATE files SET linkType = NULL, link = NULL WHERE id = ?"
).run(fileID);
const row = db.prepare("SELECT * FROM files WHERE id = ?").get(fileID);
return rowToFile(row);
};
removeLink = async (fileID: string, userID: string) => {
db.prepare(
"UPDATE files SET linkType = NULL, link = NULL WHERE id = ? AND owner = ?"
).run(fileID, userID);
return this.getFileInfo(fileID, userID);
};
makePublic = async (fileID: string, userID: string, token: string) => {
const existing: any = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
if (!existing) return null;
db.prepare(
"UPDATE files SET linkType = 'public', link = ? WHERE id = ? AND owner = ?"
).run(token, fileID, userID);
return this.getFileInfo(fileID, userID);
};
makeOneTimePublic = async (fileID: string, userID: string, token: string) => {
const existing: any = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
if (!existing) return null;
db.prepare(
"UPDATE files SET linkType = 'one', link = ? WHERE id = ? AND owner = ?"
).run(token, fileID, userID);
return this.getFileInfo(fileID, userID);
};
trashFile = async (
fileID: string,
parent: string,
parentList: string,
userID: string
) => {
const existing: any = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
if (!existing) return null;
db.prepare(
"UPDATE files SET trashed = 1, parent = ?, parentList = ? WHERE id = ? AND owner = ?"
).run(parent, parentList, fileID, userID);
return this.getFileInfo(fileID, userID);
};
restoreFile = async (fileID: string, userID: string) => {
const existing: any = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
if (!existing) return null;
db.prepare("UPDATE files SET trashed = 0 WHERE id = ? AND owner = ?").run(
fileID,
userID
);
return this.getFileInfo(fileID, userID);
};
removeTempToken = async (user: any, tempToken: string) => {
user.tempTokens = user.tempTokens.filter(
(filterToken: any) => filterToken.token !== tempToken
);
return user;
};
trashFilesByParent = async (parentList: string, userID: string) => {
const result = db
.prepare(
"UPDATE files SET trashed = 1 WHERE owner = ? AND parentList LIKE ?"
)
.run(userID, `%${parentList}%`);
return result;
};
restoreFilesByParent = async (parentList: string, userID: string) => {
const result = db
.prepare(
"UPDATE files SET trashed = 0 WHERE owner = ? AND parentList LIKE ?"
)
.run(userID, `%${parentList}%`);
return result;
};
renameFile = async (fileID: string, userID: string, title: string) => {
const existing: any = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
if (!existing) return null;
db.prepare("UPDATE files SET filename = ? WHERE id = ? AND owner = ?").run(
title,
fileID,
userID
);
return this.getFileInfo(fileID, userID);
};
moveFile = async (
fileID: string,
userID: string,
parent: string,
parentList: string
) => {
const existing: any = db
.prepare("SELECT * FROM files WHERE id = ? AND owner = ?")
.get(fileID, userID);
if (!existing) return null;
db.prepare(
"UPDATE files SET parent = ?, parentList = ? WHERE id = ? AND owner = ?"
).run(parent, parentList, fileID, userID);
return this.getFileInfo(fileID, userID);
};
moveMultipleFiles = async (
userID: string,
currentParent: string,
newParent: string,
newParentList: string
) => {
db.prepare(
"UPDATE files SET parent = ?, parentList = ? WHERE owner = ? AND parent = ?"
).run(newParent, newParentList, userID, currentParent);
};
// DELETE
deleteFile = async (fileID: string, userID: string) => {
const result = db
.prepare("DELETE FROM files WHERE id = ? AND owner = ?")
.run(fileID, userID);
return result;
};
// CREATE
createFile = async (data: {
filename: string;
uploadDate: string;
length: number;
metadata: any;
}) => {
const id = new ObjectId().toString();
const m = data.metadata || {};
db.prepare(
`INSERT INTO files (
id, filename, length, chunkSize, uploadDate, owner, parent, parentList,
hasThumbnail, isVideo, thumbnailID, size, iv, linkType, link, filePath,
s3ID, personalFile, trashed, processingFile
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
id,
data.filename,
data.length,
null,
data.uploadDate,
m.owner,
m.parent,
m.parentList,
m.hasThumbnail ? 1 : 0,
m.isVideo ? 1 : 0,
m.thumbnailID || null,
m.size,
m.IV,
m.linkType || null,
m.link || null,
m.filePath || null,
m.s3ID || null,
m.personalFile ? 1 : 0,
m.trashed ? 1 : 0,
m.processingFile ? 1 : 0
);
const row = db.prepare("SELECT * FROM files WHERE id = ?").get(id);
return rowToFile(row);
};
}
export default FileDB;