ecc340e7b1
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.
453 lines
12 KiB
TypeScript
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;
|