Skip to content
Open
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
198 changes: 198 additions & 0 deletions src/services/webdav/webdav-cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import path from 'node:path';
import { FileStatus } from '@internxt/sdk/dist/drive/storage/types';
import { DriveFolderService } from '../drive/drive-folder.service';
import { DriveFileItem, DriveFolderItem, DriveItem } from '../../types/drive.types';
import { WebDavRequestedResource } from '../../types/webdav.types';
import { DriveUtils } from '../../utils/drive.utils';
import { WebDavUtils } from '../../utils/webdav.utils';

type CacheEntry<T> = {
value: T;
expiresAt: number;
};

type FolderContent = {
folders: DriveFolderItem[];
files: DriveFileItem[];
};

export class WebDavCacheService {
public static readonly instance: WebDavCacheService = new WebDavCacheService();

private static readonly ttlMs = 2 * 60 * 1000;

private readonly items = new Map<string, CacheEntry<DriveItem>>();
private readonly folderContents = new Map<string, CacheEntry<FolderContent>>();

public getItemFromResource = async (resource: WebDavRequestedResource): Promise<DriveItem | undefined> => {
const normalizedPath = resource.url.endsWith('/')
? this.normalizeFolderPath(resource.url)
: this.normalizeFilePath(resource.url);
const cached = this.getCachedItem(normalizedPath);
if (cached) {
return cached;
}

const driveItem = await WebDavUtils.getDriveItemFromResource({ ...resource, url: normalizedPath });
if (driveItem?.status === FileStatus.EXISTS) {
this.setItem(driveItem.itemType === 'folder' ? this.normalizeFolderPath(normalizedPath) : normalizedPath, driveItem);
return driveItem;
}
};

public getFileFromPath = async (filePath: string): Promise<DriveFileItem | undefined> => {
const normalizedPath = this.normalizeFilePath(filePath);
const cached = this.getCachedItem(normalizedPath);
if (cached?.itemType === 'file') {
return cached;
}

const driveFile = await WebDavUtils.getDriveFileFromResource(normalizedPath);
if (driveFile?.status === FileStatus.EXISTS) {
this.setItem(normalizedPath, driveFile);
return driveFile;
}
};

public getFolderFromPath = async (folderPath: string): Promise<DriveFolderItem | undefined> => {
const normalizedPath = this.normalizeFolderPath(folderPath);
const cached = this.getCachedItem(normalizedPath);
if (cached?.itemType === 'folder') {
return cached;
}

const driveFolder = await WebDavUtils.getDriveFolderFromResource(normalizedPath);
if (driveFolder?.status === FileStatus.EXISTS) {
this.setItem(normalizedPath, driveFolder);
return driveFolder;
}
};

public getFolderContent = async (folderPath: string, folderUuid: string): Promise<FolderContent> => {
const normalizedPath = this.normalizeFolderPath(folderPath);
const cached = this.getCachedFolderContent(normalizedPath);
if (cached) {
return cached;
}

const folderContent = await DriveFolderService.instance.getFolderContent(folderUuid);
const mappedContent: FolderContent = {
folders: folderContent.folders.map((folder) => ({
itemType: 'folder',
name: folder.plainName,
bucket: folder.bucket,
status: folder.deleted || folder.removed ? FileStatus.TRASHED : FileStatus.EXISTS,
createdAt: new Date(folder.createdAt),
updatedAt: new Date(folder.updatedAt),
creationTime: new Date(folder.creationTime),
modificationTime: new Date(folder.modificationTime),
uuid: folder.uuid,
parentUuid: folder.parentUuid,
})),
files: folderContent.files.map((file) => ({
itemType: 'file',
name: file.plainName,
bucket: file.bucket,
fileId: file.fileId,
uuid: file.uuid,
type: file.type,
status: file.status,
folderUuid: file.folderUuid,
size: DriveUtils.parseFileSize(file.size),
creationTime: new Date(file.creationTime),
modificationTime: new Date(file.modificationTime),
createdAt: new Date(file.createdAt),
updatedAt: new Date(file.updatedAt),
})),
};

for (const folder of mappedContent.folders) {
this.setItem(WebDavUtils.joinURL(normalizedPath, folder.name, '/'), folder);
}
for (const file of mappedContent.files) {
const fileName = file.type ? `${file.name}.${file.type}` : file.name;
this.setItem(WebDavUtils.joinURL(normalizedPath, fileName), file);
}

this.setFolderContent(normalizedPath, mappedContent);
return mappedContent;
};

public registerFile = (filePath: string, file: DriveFileItem): void => {
const normalizedPath = this.normalizeFilePath(filePath);
this.setItem(normalizedPath, file);
this.invalidateFolderContent(this.getParentFolderPath(normalizedPath));
};

public registerFolder = (folderPath: string, folder: DriveFolderItem): void => {
const normalizedPath = this.normalizeFolderPath(folderPath);
this.setItem(normalizedPath, folder);
this.invalidateFolderContent(this.getParentFolderPath(normalizedPath));
};

public invalidateResource = (resourcePath: string): void => {
const itemPath = resourcePath.endsWith('/') ? this.normalizeFolderPath(resourcePath) : this.normalizeFilePath(resourcePath);
this.items.delete(itemPath);
this.folderContents.delete(this.normalizeFolderPath(resourcePath));
this.invalidateFolderContent(this.getParentFolderPath(itemPath));
};

public invalidateFolderContent = (folderPath: string): void => {
this.folderContents.delete(this.normalizeFolderPath(folderPath));
};

public clear = (): void => {
this.items.clear();
this.folderContents.clear();
};

private getCachedItem(path: string): DriveItem | undefined {
return this.getCachedValue(this.items, path);
}

private getCachedFolderContent(path: string): FolderContent | undefined {
return this.getCachedValue(this.folderContents, path);
}

private getCachedValue<T>(cache: Map<string, CacheEntry<T>>, key: string): T | undefined {
const entry = cache.get(key);
if (!entry) {
return;
}

if (entry.expiresAt <= Date.now()) {
cache.delete(key);
return;
}

return entry.value;
}

private setItem(path: string, value: DriveItem): void {
this.items.set(path, this.createEntry(value));
}

private setFolderContent(path: string, value: FolderContent): void {
this.folderContents.set(path, this.createEntry(value));
}

private createEntry<T>(value: T): CacheEntry<T> {
return {
value,
expiresAt: Date.now() + WebDavCacheService.ttlMs,
};
}

private normalizeFilePath(filePath: string): string {
const normalizedPath = path.posix.normalize(filePath);
return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
}

private normalizeFolderPath(folderPath: string): string {
return WebDavUtils.normalizeFolderPath(this.normalizeFilePath(folderPath));
}

private getParentFolderPath(resourcePath: string): string {
return WebDavUtils.normalizeFolderPath(path.posix.dirname(resourcePath));
}
}
4 changes: 3 additions & 1 deletion src/services/webdav/webdav-folder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { WebDavUtils } from '../../utils/webdav.utils';
import { AsyncUtils } from '../../utils/async.utils';
import { AuthService } from '../../services/auth.service';
import { DriveUtils } from '../../utils/drive.utils';
import { WebDavCacheService } from './webdav-cache.service';

export class WebDavFolderService {
public static readonly instance: WebDavFolderService = new WebDavFolderService();

public getDriveFolderItemFromPath = async (path: string): Promise<DriveFolderItem | undefined> => {
const { url } = await WebDavUtils.getRequestedResource(path, false);
return await WebDavUtils.getDriveFolderFromResource(url);
return await WebDavCacheService.instance.getFolderFromPath(url);
};

public createFolder = async ({
Expand Down Expand Up @@ -65,6 +66,7 @@ export class WebDavFolderService {
const folder =
(await this.getDriveFolderItemFromPath(folderPath)) ??
(await this.createFolder({ folderName: currentFolderName, parentFolderUuid }));
WebDavCacheService.instance.registerFolder(folderPath, folder);

if (rest.length === 0) {
return folder;
Expand Down
4 changes: 3 additions & 1 deletion src/webdav/handlers/DELETE.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import { WebDavMethodHandler } from '../../types/webdav.types';
import { WebDavUtils } from '../../utils/webdav.utils';
import { webdavLogger } from '../../utils/logger.utils';
import { NotFoundError } from '../../utils/errors.utils';
import { WebDavCacheService } from '../../services/webdav/webdav-cache.service';

export class DELETERequestHandler implements WebDavMethodHandler {
handle = async (req: Request, res: Response) => {
const resource = await WebDavUtils.getRequestedResource(req.url);
webdavLogger.info(`[DELETE] Request received for item at ${resource.url}`);

const driveItem = await WebDavUtils.getDriveItemFromResource(resource);
const driveItem = await WebDavCacheService.instance.getItemFromResource(resource);

if (!driveItem) {
throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
}

await WebDavUtils.deleteOrTrashItem(driveItem);
await DriveItemRepository.instance.delete([driveItem.uuid]);
WebDavCacheService.instance.invalidateResource(resource.url);

res.status(204).send();
};
Expand Down
3 changes: 2 additions & 1 deletion src/webdav/handlers/GET.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { webdavLogger } from '../../utils/logger.utils';
import { NetworkUtils } from '../../utils/network.utils';
import { NotValidFileIdError } from '../../types/command.types';
import { CLIUtils } from '../../utils/cli.utils';
import { WebDavCacheService } from '../../services/webdav/webdav-cache.service';

export class GETRequestHandler implements WebDavMethodHandler {
handle = async (req: Request, res: Response) => {
const resource = await WebDavUtils.getRequestedResource(req.url);

webdavLogger.info(`[GET] Request received item at ${resource.url}`);
const driveFile = await WebDavUtils.getDriveFileFromResource(resource.url);
const driveFile = await WebDavCacheService.instance.getFileFromPath(resource.url);

if (!driveFile) {
throw new NotFoundError(
Expand Down
3 changes: 2 additions & 1 deletion src/webdav/handlers/HEAD.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { WebDavUtils } from '../../utils/webdav.utils';
import { webdavLogger } from '../../utils/logger.utils';
import { NetworkUtils } from '../../utils/network.utils';
import { NotFoundError } from '../../utils/errors.utils';
import { WebDavCacheService } from '../../services/webdav/webdav-cache.service';

export class HEADRequestHandler implements WebDavMethodHandler {
handle = async (req: Request, res: Response) => {
const resource = await WebDavUtils.getRequestedResource(req.url);

webdavLogger.info(`[HEAD] Request received for item at ${resource.url}`);

const driveItem = await WebDavUtils.getDriveItemFromResource(resource);
const driveItem = await WebDavCacheService.instance.getItemFromResource(resource);

if (!driveItem) {
throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
Expand Down
6 changes: 4 additions & 2 deletions src/webdav/handlers/MKCOL.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { XMLUtils } from '../../utils/xml.utils';
import { WebDavFolderService } from '../../services/webdav/webdav-folder.service';
import { AsyncUtils } from '../../utils/async.utils';
import { MethodNotAllowed } from '../../utils/errors.utils';
import { WebDavCacheService } from '../../services/webdav/webdav-cache.service';

export class MKCOLRequestHandler implements WebDavMethodHandler {
handle = async (req: Request, res: Response) => {
Expand All @@ -15,10 +16,10 @@ export class MKCOLRequestHandler implements WebDavMethodHandler {
webdavLogger.info(`[MKCOL] Request received for folder at ${resource.url}`);

const parentDriveFolderItem =
(await WebDavFolderService.instance.getDriveFolderItemFromPath(resource.parentPath)) ??
(await WebDavCacheService.instance.getFolderFromPath(resource.parentPath)) ??
(await WebDavFolderService.instance.createParentPathOrThrow(resource.parentPath));

const driveFolderItem = await WebDavUtils.getDriveFolderFromResource(resource.url);
const driveFolderItem = await WebDavCacheService.instance.getFolderFromPath(resource.url);

const folderAlreadyExists = !!driveFolderItem;

Expand All @@ -45,6 +46,7 @@ export class MKCOLRequestHandler implements WebDavMethodHandler {
updatedAt: new Date(),
},
]);
WebDavCacheService.instance.registerFolder(resource.url, newFolder);

// This aims to prevent this issue: https://inxt.atlassian.net/browse/PB-1446
await AsyncUtils.sleep(500);
Expand Down
5 changes: 4 additions & 1 deletion src/webdav/handlers/MOVE.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { NotFoundError } from '../../utils/errors.utils';
import { webdavLogger } from '../../utils/logger.utils';
import { WebDavUtils } from '../../utils/webdav.utils';
import { WebDavFolderService } from '../../services/webdav/webdav-folder.service';
import { WebDavCacheService } from '../../services/webdav/webdav-cache.service';

export class MOVERequestHandler implements WebDavMethodHandler {
handle = async (req: Request, res: Response) => {
Expand All @@ -23,7 +24,7 @@ export class MOVERequestHandler implements WebDavMethodHandler {

webdavLogger.info('[MOVE] Destination resource found', { destinationResource });

const originalDriveItem = await WebDavUtils.getDriveItemFromResource(resource);
const originalDriveItem = await WebDavCacheService.instance.getItemFromResource(resource);

if (!originalDriveItem) {
throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
Expand Down Expand Up @@ -89,6 +90,8 @@ export class MOVERequestHandler implements WebDavMethodHandler {
updatedAt: new Date(),
},
]);
WebDavCacheService.instance.invalidateResource(resource.url);
WebDavCacheService.instance.invalidateResource(destinationResource.url);

res.status(204).send();
};
Expand Down
Loading