Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions WEBDAV.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Below you can find a list of WebDav clients that we officially support in the In
| CyberDuck for MacOS | ✅ |
| Transmit | ✅ |
| Cadaver | ✅ |
| Nautilus (GNOME Files)| ✅ |

## Supported WebDav methods

Expand Down
1 change: 1 addition & 0 deletions src/services/drive/drive-file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class DriveFileService {
const driveFile = await storageClient.createFileEntryByUuid(payload);

return {
itemType: 'file',
name: payload.plainName,
id: driveFile.id,
uuid: driveFile.uuid,
Expand Down
2 changes: 2 additions & 0 deletions src/types/drive.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type DriveFileItem = Omit<
| 'modificationTime'
| 'type'
> & {
itemType: 'file';
size: number;
createdAt: Date;
updatedAt: Date;
Expand All @@ -21,6 +22,7 @@ export type DriveFileItem = Omit<
};

export type DriveFolderItem = Pick<DriveFolderData, 'name' | 'bucket' | 'id' | 'parentId'> & {
itemType: 'folder';
encryptedName: string;
uuid: string;
createdAt: Date;
Expand Down
1 change: 0 additions & 1 deletion src/types/webdav.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export type WebDavMethodHandlerOptions = {
};

export type WebDavRequestedResource = {
type: 'file' | 'folder';
url: string;
name: string;
path: ParsedPath;
Expand Down
3 changes: 3 additions & 0 deletions src/utils/drive.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DriveFileItem, DriveFolderItem } from '../types/drive.types';
export class DriveUtils {
static driveFileMetaToItem(fileMeta: FileMeta): DriveFileItem {
return {
itemType: 'file',
uuid: fileMeta.uuid ?? '',
status: fileMeta.status,
folderId: fileMeta.folderId,
Expand All @@ -23,6 +24,7 @@ export class DriveUtils {

static driveFolderMetaToItem(folderMeta: FolderMeta): DriveFolderItem {
return {
itemType: 'folder',
uuid: folderMeta.uuid,
id: folderMeta.id,
bucket: folderMeta.bucket,
Expand All @@ -38,6 +40,7 @@ export class DriveUtils {

static createFolderResponseToItem(folderResponse: CreateFolderResponse): DriveFolderItem {
return {
itemType: 'folder',
uuid: folderResponse.uuid,
id: folderResponse.id,
bucket: folderResponse.bucket,
Expand Down
109 changes: 62 additions & 47 deletions src/utils/webdav.utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Request } from 'express';
import path from 'node:path';
import { WebDavRequestedResource } from '../types/webdav.types';
import { DriveFolderService } from '../services/drive/drive-folder.service';
import { DriveFileService } from '../services/drive/drive-file.service';
import { DriveFileItem, DriveFolderItem, DriveItem } from '../types/drive.types';
import { webdavLogger } from './logger.utils';

export class WebDavUtils {
static joinURL(...pathComponents: string[]): string {
Expand Down Expand Up @@ -39,74 +39,89 @@ export class WebDavUtils {
return normalizedPath;
}

static async getRequestedResource(urlObject: string | Request, decodeUri = true): Promise<WebDavRequestedResource> {
let requestUrl: string;
if (typeof urlObject === 'string') {
requestUrl = urlObject;
} else {
requestUrl = urlObject.url;
}

// TODO: We can rename this method to parseUrlToPathMetadata so its more descriptive
static async getRequestedResource(requestUrl: string, decodeUri = true): Promise<WebDavRequestedResource> {
const decodedUrl = this.decodeUrl(requestUrl, decodeUri);
const parsedPath = path.parse(decodedUrl);
const parentPath = this.normalizeFolderPath(path.dirname(decodedUrl));

const isFolder = requestUrl.endsWith('/');

if (isFolder) {
return {
type: 'folder',
url: decodedUrl,
name: parsedPath.base,
path: parsedPath,
parentPath,
};
} else {
return {
type: 'file',
url: decodedUrl,
name: parsedPath.name,
path: parsedPath,
parentPath,
};
}
return {
url: decodedUrl,
name: parsedPath.base,
path: parsedPath,
parentPath,
};
}

static async getDriveItemFromResource(params: {
resource: WebDavRequestedResource;
static async tryGetFileOrFolderMetadata({
url,
driveFileService,
driveFolderService,
}: {
url: string;
driveFolderService: DriveFolderService;
driveFileService?: never;
}): Promise<DriveFolderItem | undefined>;
driveFileService: DriveFileService;
}): Promise<DriveItem | undefined> {
try {
return await driveFileService.getFileMetadataByPath(url);
} catch {
return await driveFolderService.getFolderMetadataByPath(url);
}
}

static async getDriveItemFromResource(params: {
resource: WebDavRequestedResource;
driveFolderService?: never;
static async getDriveFileFromResource({
url,
driveFileService,
}: {
url: string;
driveFileService: DriveFileService;
}): Promise<DriveFileItem | undefined>;
}): Promise<DriveFileItem | undefined> {
try {
return await driveFileService.getFileMetadataByPath(url);
} catch (err) {
webdavLogger.error('Exception while getting the file metadata by path', err);
}
}

static async getDriveItemFromResource(params: {
resource: WebDavRequestedResource;
static async getDriveFolderFromResource({
url,
driveFolderService,
}: {
url: string;
driveFolderService: DriveFolderService;
driveFileService: DriveFileService;
}): Promise<DriveItem | undefined>;
}): Promise<DriveFolderItem | undefined> {
try {
return await driveFolderService.getFolderMetadataByPath(url);
} catch (err) {
webdavLogger.error('Exception while getting the folder metadata by path', err);
}
}

// This method will be used mainly for propfind, since we dont know what the user is requesting for
static async getDriveItemFromResource({
resource,
driveFolderService,
driveFileService,
}: {
resource: WebDavRequestedResource;
driveFolderService?: DriveFolderService;
driveFileService?: DriveFileService;
driveFolderService: DriveFolderService;
driveFileService: DriveFileService;
}): Promise<DriveItem | undefined> {
let item: DriveItem | undefined = undefined;

// This is exactly the problem, nautilus sends folder urls without trailing slash
// There is just no other way to check wether its a folder or file at this point other than checking ourselves
const isFolder = resource.url.endsWith('/');

try {
if (resource.type === 'folder') {
item = await driveFolderService?.getFolderMetadataByPath(resource.url);
}
if (resource.type === 'file') {
item = await driveFileService?.getFileMetadataByPath(resource.url);
if (isFolder) {
item = await driveFolderService.getFolderMetadataByPath(resource.url);
} else {
item = await this.tryGetFileOrFolderMetadata({
url: resource.url,
driveFileService,
driveFolderService,
});
}
} catch {
//no op
Expand Down
10 changes: 5 additions & 5 deletions src/webdav/handlers/DELETE.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export class DELETERequestHandler implements WebDavMethodHandler {

handle = async (req: Request, res: Response) => {
const { driveFileService, driveFolderService, trashService } = this.dependencies;
const resource = await WebDavUtils.getRequestedResource(req);
webdavLogger.info(`[DELETE] Request received for ${resource.type} at ${resource.url}`);
const resource = await WebDavUtils.getRequestedResource(req.url);
webdavLogger.info(`[DELETE] Request received for item at ${resource.url}`);

const driveItem = await WebDavUtils.getDriveItemFromResource({
resource,
Expand All @@ -31,13 +31,13 @@ export class DELETERequestHandler implements WebDavMethodHandler {
throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
}

webdavLogger.info(`[DELETE] [${driveItem.uuid}] Trashing ${resource.type}`);
webdavLogger.info(`[DELETE] [${driveItem.uuid}] Trashing ${driveItem.itemType}`);
await trashService.trashItems({
items: [{ type: resource.type, uuid: driveItem.uuid, id: null }],
items: [{ type: driveItem.itemType, uuid: driveItem.uuid, id: null }],
});

res.status(204).send();
const type = resource.type.charAt(0).toUpperCase() + resource.type.substring(1);
const type = driveItem.itemType.charAt(0).toUpperCase() + driveItem.itemType.substring(1);
webdavLogger.info(`[DELETE] [${driveItem.uuid}] ${type} trashed successfully`);
};
}
17 changes: 8 additions & 9 deletions src/webdav/handlers/GET.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { CryptoService } from '../../services/crypto.service';
import { AuthService } from '../../services/auth.service';
import { NotFoundError } from '../../utils/errors.utils';
import { webdavLogger } from '../../utils/logger.utils';
import { DriveFileItem } from '../../types/drive.types';
import { NetworkUtils } from '../../utils/network.utils';

export class GETRequestHandler implements WebDavMethodHandler {
Expand All @@ -24,21 +23,21 @@ export class GETRequestHandler implements WebDavMethodHandler {

handle = async (req: Request, res: Response) => {
const { driveFileService, authService, networkFacade } = this.dependencies;
const resource = await WebDavUtils.getRequestedResource(req);
const resource = await WebDavUtils.getRequestedResource(req.url);

if (resource.name.startsWith('._')) throw new NotFoundError('File not found');
if (resource.type === 'folder') throw new NotFoundError('Folders cannot be listed with GET. Use PROPFIND instead.');

webdavLogger.info(`[GET] Request received for ${resource.type} at ${resource.url}`);
const driveItem = await WebDavUtils.getDriveItemFromResource({
resource,
webdavLogger.info(`[GET] Request received item at ${resource.url}`);
const driveFile = await WebDavUtils.getDriveFileFromResource({
url: resource.url,
driveFileService,
});

if (!driveItem) {
throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`);
if (!driveFile) {
throw new NotFoundError(
`Resource not found on Internxt Drive at ${resource.url}, if trying to access a folder use PROPFIND instead.`,
);
}
const driveFile = driveItem as DriveFileItem;

webdavLogger.info(`[GET] [${driveFile.uuid}] Found Drive File`);

Expand Down
17 changes: 5 additions & 12 deletions src/webdav/handlers/HEAD.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { WebDavMethodHandler } from '../../types/webdav.types';
import { WebDavUtils } from '../../utils/webdav.utils';
import { webdavLogger } from '../../utils/logger.utils';
import { DriveFileService } from '../../services/drive/drive-file.service';
import { DriveFileItem } from '../../types/drive.types';
import { NetworkUtils } from '../../utils/network.utils';
import { NotFoundError } from '../../utils/errors.utils';

Expand All @@ -16,25 +15,19 @@ export class HEADRequestHandler implements WebDavMethodHandler {

handle = async (req: Request, res: Response) => {
const { driveFileService } = this.dependencies;
const resource = await WebDavUtils.getRequestedResource(req);
const resource = await WebDavUtils.getRequestedResource(req.url);

if (resource.type === 'folder') {
res.status(200).send();
return;
}

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

try {
const driveItem = await WebDavUtils.getDriveItemFromResource({
resource,
const driveFile = await WebDavUtils.getDriveFileFromResource({
url: resource.url,
driveFileService,
});

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

webdavLogger.info(`[HEAD] [${driveFile.uuid}] Found Drive File`);

Expand Down
8 changes: 4 additions & 4 deletions src/webdav/handlers/MKCOL.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ export class MKCOLRequestHandler implements WebDavMethodHandler {

handle = async (req: Request, res: Response) => {
const { driveFolderService, webDavFolderService } = this.dependencies;
const resource = await WebDavUtils.getRequestedResource(req);
const resource = await WebDavUtils.getRequestedResource(req.url);

webdavLogger.info(`[MKCOL] Request received for ${resource.type} at ${resource.url}`);
webdavLogger.info(`[MKCOL] Request received for folder at ${resource.url}`);

const parentDriveFolderItem =
(await webDavFolderService.getDriveFolderItemFromPath(resource.parentPath)) ??
(await webDavFolderService.createParentPathOrThrow(resource.parentPath));

const driveFolderItem = await WebDavUtils.getDriveItemFromResource({
resource,
const driveFolderItem = await WebDavUtils.getDriveFolderFromResource({
url: resource.url,
driveFolderService,
});

Expand Down
Loading
Loading