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.
636 lines
13 KiB
TypeScript
636 lines
13 KiB
TypeScript
import { NextFunction, Request, Response } from "express";
|
|
import FileService from "../services/file-service/file-service";
|
|
import { UserInterface } from "../models/user-model";
|
|
import {
|
|
createStreamVideoCookie,
|
|
removeStreamVideoCookie,
|
|
} from "../cookies/create-cookies";
|
|
import ChunkService from "../services/chunk-service/chunk-service";
|
|
import streamToBuffer from "../utils/streamToBuffer";
|
|
import NotAuthorizedError from "../utils/NotAuthorizedError";
|
|
import { FileListQueryType } from "../types/file-types";
|
|
import fs from "fs";
|
|
import { getUserDB } from "../db/dbFactory";
|
|
|
|
const fileService = new FileService();
|
|
const userDB = getUserDB();
|
|
type userAccessType = {
|
|
_id: string;
|
|
emailVerified: boolean;
|
|
email: string;
|
|
s3Enabled: boolean;
|
|
};
|
|
|
|
export interface RequestTypeFullUser extends Request {
|
|
user?: UserInterface;
|
|
encryptedToken?: string;
|
|
accessTokenStreamVideo?: string;
|
|
}
|
|
|
|
interface RequestType extends Request {
|
|
user?: userAccessType;
|
|
encryptedToken?: string;
|
|
}
|
|
|
|
class FileController {
|
|
chunkService;
|
|
|
|
constructor() {
|
|
this.chunkService = new ChunkService();
|
|
}
|
|
|
|
getThumbnail = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
try {
|
|
const user = req.user;
|
|
const id = req.params.id;
|
|
|
|
await this.chunkService.getThumbnail(user, id, res);
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getFullThumbnail = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
try {
|
|
const user = req.user;
|
|
const fileID = req.params.id;
|
|
|
|
await this.chunkService.getFullThumbnail(user, fileID, res);
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
uploadFile = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = req.user;
|
|
const busboy = req.busboy;
|
|
|
|
const file = await this.chunkService.uploadFile(user, busboy, req);
|
|
|
|
res.send(file);
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getPublicDownload = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
try {
|
|
const ID = req.params.id;
|
|
const tempToken = req.params.tempToken;
|
|
|
|
await this.chunkService.getPublicDownload(ID, tempToken, res);
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
removeLink = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const id = req.params.id;
|
|
const userID = req.user._id;
|
|
|
|
const file = await fileService.removeLink(userID, id);
|
|
|
|
res.send(file);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
makePublic = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const fileID = req.params.id;
|
|
const userID = req.user._id;
|
|
|
|
const { file, token } = await fileService.makePublic(userID, fileID);
|
|
|
|
res.send({ file, token });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getPublicInfo = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const tempToken = req.params.tempToken;
|
|
|
|
const file = await fileService.getPublicInfo(id, tempToken);
|
|
|
|
res.send(file);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
makeOneTimePublic = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const id = req.params.id;
|
|
const userID = req.user._id;
|
|
|
|
const { file, token } = await fileService.makeOneTimePublic(userID, id);
|
|
|
|
res.send({ file, token });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getFileInfo = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const fileID = req.params.id;
|
|
const userID = req.user._id;
|
|
|
|
const file = await fileService.getFileInfo(userID, fileID);
|
|
|
|
res.send(file);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getQuickList = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = req.user;
|
|
const limit = Number.parseInt(req.query.limit as string) || 20;
|
|
|
|
const quickList = await fileService.getQuickList(user, limit);
|
|
|
|
res.send(quickList);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getList = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const query = req.query;
|
|
|
|
const search = (query.search as string) || undefined;
|
|
const parent = (query.parent as string) || "/";
|
|
const limit = Number.parseInt(query.limit as string) || 50;
|
|
const sortBy = (query.sortBy as string) || "date_desc";
|
|
const startAtDate = (query.startAtDate as string) || undefined;
|
|
const startAtName = (query.startAtName as string) || undefined;
|
|
const trashMode = query.trashMode === "true";
|
|
const mediaMode = query.mediaMode === "true";
|
|
const mediaFilter = (query.mediaFilter as string) || "all";
|
|
|
|
const queryData: FileListQueryType = {
|
|
userID,
|
|
search,
|
|
parent,
|
|
startAtDate,
|
|
startAtName,
|
|
trashMode,
|
|
mediaMode,
|
|
sortBy,
|
|
mediaFilter,
|
|
};
|
|
|
|
const fileList = await fileService.getList(queryData, sortBy, limit);
|
|
|
|
res.send(fileList);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getDownloadToken = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = req.user;
|
|
|
|
const tempToken = await fileService.getDownloadToken(user);
|
|
|
|
res.send({ tempToken });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getAccessTokenStreamVideo = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) return;
|
|
|
|
try {
|
|
const user = req.user;
|
|
|
|
const currentUUID = req.headers.uuid as string;
|
|
|
|
const streamVideoAccessToken = await user.generateAuthTokenStreamVideo(
|
|
currentUUID
|
|
);
|
|
|
|
createStreamVideoCookie(res, streamVideoAccessToken);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
removeStreamVideoAccessToken = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) return;
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
|
|
const accessTokenStreamVideo = req.accessTokenStreamVideo!;
|
|
|
|
if (!accessTokenStreamVideo) {
|
|
throw new NotAuthorizedError("No Access Token");
|
|
}
|
|
|
|
await userDB.removeTempTokenByValue(
|
|
userID.toString(),
|
|
accessTokenStreamVideo
|
|
);
|
|
|
|
removeStreamVideoCookie(res);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
removeTempToken = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = req.user;
|
|
const tempToken = req.params.tempToken;
|
|
const currentUUID = req.params.uuid;
|
|
|
|
await fileService.removeTempToken(user, tempToken, currentUUID);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
streamVideo = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = req.user;
|
|
const fileID = req.params.id;
|
|
const headers = req.headers;
|
|
|
|
await this.chunkService.streamVideo(user, fileID, headers, res);
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
streamVideoTest = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const headers = req.headers;
|
|
console.log("headers", headers.range);
|
|
const fileSize = 26867866;
|
|
const range = headers.range!;
|
|
const parts = range.replace(/bytes=/, "").split("-");
|
|
const start = parseInt(parts[0], 10);
|
|
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
|
const chunksize = end - start + 1;
|
|
|
|
const readStream = fs.createReadStream(
|
|
"/Users/kylehoell/Developer/myDrive-4/upgrade/old/video.mp4",
|
|
{ start, end }
|
|
);
|
|
|
|
const head = {
|
|
"Content-Range": "bytes " + start + "-" + end + "/" + fileSize,
|
|
"Accept-Ranges": "bytes",
|
|
"Content-Length": chunksize,
|
|
"Content-Type": "video/mp4",
|
|
};
|
|
|
|
res.writeHead(206, head);
|
|
|
|
readStream.on("data", (data) => {
|
|
res.write(data);
|
|
});
|
|
|
|
readStream.on("end", () => {
|
|
res.end();
|
|
});
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
downloadFile = async (
|
|
req: RequestTypeFullUser,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = req.user;
|
|
const fileID = req.params.id;
|
|
|
|
await this.chunkService.downloadFile(user, fileID, res);
|
|
} catch (e: unknown) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
getSuggestedList = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const searchQuery = req.query.search as string;
|
|
const trashMode = req.query.trashMode === "true";
|
|
const mediaMode = req.query.mediaMode === "true";
|
|
|
|
const { fileList, folderList } = await fileService.getSuggestedList(
|
|
userID,
|
|
searchQuery,
|
|
trashMode,
|
|
mediaMode
|
|
);
|
|
|
|
return res.send({ folderList, fileList });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
renameFile = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const fileID = req.body.id;
|
|
const title = req.body.title;
|
|
const userID = req.user._id;
|
|
|
|
const file = await fileService.renameFile(userID, fileID, title);
|
|
|
|
res.send(file);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
moveFile = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const fileID = req.body.id as string;
|
|
const userID = req.user._id as string;
|
|
const parentID = (req.body.parentID as string) || "/";
|
|
|
|
const file = await fileService.moveFile(userID, fileID, parentID);
|
|
|
|
res.send(file);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
moveMultiFile = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const items = req.body.items;
|
|
const parentID = (req.body.parentID as string) || "/";
|
|
|
|
await fileService.moveMultiFiles(userID, items, parentID);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
trashFile = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const fileID = req.body.id;
|
|
|
|
const trashedFile = await fileService.trashFile(userID, fileID);
|
|
|
|
res.send(trashedFile);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
restoreFile = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const fileID = req.body.id;
|
|
|
|
const file = await fileService.restoreFile(userID, fileID);
|
|
|
|
res.send(file);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
deleteFile = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const fileID = req.body.id;
|
|
|
|
await this.chunkService.deleteFile(userID, fileID);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
deleteMulti = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const items = req.body.items;
|
|
|
|
await this.chunkService.deleteMulti(userID, items);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
trashMulti = async (req: RequestType, res: Response, next: NextFunction) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const items = req.body.items;
|
|
|
|
await fileService.trashMulti(userID, items);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
|
|
restoreMulti = async (
|
|
req: RequestType,
|
|
res: Response,
|
|
next: NextFunction
|
|
) => {
|
|
if (!req.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const userID = req.user._id;
|
|
const items = req.body.items;
|
|
|
|
await fileService.restoreMulti(userID, items);
|
|
|
|
res.send();
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
};
|
|
}
|
|
|
|
export default FileController;
|