Skip to content
Merged
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
5 changes: 1 addition & 4 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ DRIVE_URL=https://drive.internxt.com
DRIVE_API_URL=https://drive.internxt.com/api
DRIVE_NEW_API_URL=https://api.internxt.com/drive
PAYMENTS_API_URL=https://api.internxt.com/payments
PHOTOS_API_URL=https://photos.internxt.com/api
NETWORK_URL=https://api.internxt.com
APP_CRYPTO_SECRET=6KYQBP847D4ATSFA
APP_CRYPTO_SECRET2=8Q8VMUE3BJZV87GT
APP_MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023
APP_MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e
RUDDERSTACK_WRITE_KEY=
RUDDERSTACK_DATAPLANE_URL=
APP_MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e
3 changes: 0 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@ jobs:
echo "DRIVE_API_URL=https://drive.internxt.com/api" >> .env
echo "DRIVE_NEW_API_URL=https://api.internxt.com/drive" >> .env
echo "PAYMENTS_API_URL=https://api.internxt.com/payments" >> .env
echo "PHOTOS_API_URL=https://photos.internxt.com/api" >> .env
echo "NETWORK_URL=https://api.internxt.com" >> .env
echo "APP_CRYPTO_SECRET=6KYQBP847D4ATSFA" >> .env
echo "APP_CRYPTO_SECRET2=8Q8VMUE3BJZV87GT" >> .env
echo "APP_MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023" >> .env
echo "APP_MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e" >> .env
echo "RUDDERSTACK_WRITE_KEY=" >> .env
echo "RUDDERSTACK_DATAPLANE_URL=" >> .env
echo "NODE_ENV=production" >> .env

- run: yarn run build
Expand Down
3 changes: 0 additions & 3 deletions src/types/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ export interface ConfigKeys {
readonly DRIVE_API_URL: string;
readonly DRIVE_NEW_API_URL: string;
readonly PAYMENTS_API_URL: string;
readonly PHOTOS_API_URL: string;
readonly APP_CRYPTO_SECRET: string;
readonly APP_CRYPTO_SECRET2: string;
readonly APP_MAGIC_IV: string;
readonly APP_MAGIC_SALT: string;
readonly NETWORK_URL: string;
readonly RUDDERSTACK_WRITE_KEY: string;
readonly RUDDERSTACK_DATAPLANE_URL: string;
}
19 changes: 17 additions & 2 deletions src/utils/webdav.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { DriveDatabaseManager } from '../services/database/drive-database-manage
import { DriveFolderService } from '../services/drive/drive-folder.service';
import { DriveFileService } from '../services/drive/drive-file.service';
import { DriveFileItem, DriveFolderItem } from '../types/drive.types';
import { NotFoundError } from './errors.utils';
import { ConflictError, NotFoundError } from './errors.utils';
import { webdavLogger } from './logger.utils';
import AppError from '@internxt/sdk/dist/shared/types/errors';

export class WebDavUtils {
static joinURL(...pathComponents: string[]): string {
Expand All @@ -34,6 +35,7 @@ export class WebDavUtils {
} else {
requestUrl = urlObject.url;
}

const decodedUrl = decodeURIComponent(requestUrl).replaceAll('/./', '/');
const parsedPath = path.parse(decodedUrl);
let parentPath = path.dirname(decodedUrl);
Expand Down Expand Up @@ -95,8 +97,21 @@ export class WebDavUtils {
driveFileService?: DriveFileService,
): Promise<DriveFileItem | DriveFolderItem | undefined> {
let item: DriveFileItem | DriveFolderItem | undefined = undefined;

if (resource.type === 'folder') {
item = await driveFolderService?.getFolderMetadataByPath(resource.url);
// if resource has a parentPath it means it's a subfolder then try to get it; if it throws an error it means it doesn't
// exist and we should throw a 409 error in compliance with the WebDAV RFC
// catch the error during getting parent folder and throw a 409 error in compliance with the WebDAV RFC
try {
item = await driveFolderService?.getFolderMetadataByPath(resource.url);
} catch (error) {
// if the error is a 404 error, it means the resource doesn't exist
// in this case, throw a 409 error in compliance with the WebDAV RFC
if ((error as AppError).status === 404) {
throw new ConflictError(`Resource not found on Internxt Drive at ${resource.url}`);
}
throw error;
}
}
if (resource.type === 'file') {
item = await driveFileService?.getFileMetadataByPath(resource.url);
Expand Down
18 changes: 18 additions & 0 deletions src/webdav/handlers/MKCOL.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { webdavLogger } from '../../utils/logger.utils';
import { XMLUtils } from '../../utils/xml.utils';
import { AsyncUtils } from '../../utils/async.utils';
import { DriveFolderItem } from '../../types/drive.types';
import { MethodNotAllowed } from '../../utils/errors.utils';
import AppError from '@internxt/sdk/dist/shared/types/errors';

export class MKCOLRequestHandler implements WebDavMethodHandler {
constructor(
Expand All @@ -30,6 +32,22 @@ export class MKCOLRequestHandler implements WebDavMethodHandler {
driveFolderService,
})) as DriveFolderItem;

let folderAlreadyExists = true;
// try to get the folder from the drive before creating it
// The method getFolderMetadataByPath will throw an error if the folder does not exist, so we need to catch it
try {
await driveFolderService.getFolderMetadataByPath(resource.url);
} catch (error) {
if ((error as AppError).status === 404) {
folderAlreadyExists = false;
}
}

if (folderAlreadyExists) {
webdavLogger.info(`[MKCOL] ❌ Folder '${resource.url}' already exists`);
throw new MethodNotAllowed('Folder already exists');
}

const [createFolder] = driveFolderService.createFolder({
plainName: resource.path.base,
parentFolderUuid: parentFolderItem.uuid,
Expand Down
2 changes: 1 addition & 1 deletion src/webdav/handlers/PUT.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,6 @@ export class PUTRequestHandler implements WebDavMethodHandler {

await driveDatabaseManager.createFile(file, resource.path.dir + '/');

res.status(200).send();
res.status(201).send();
};
}
25 changes: 25 additions & 0 deletions src/webdav/middewares/mkcol.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { RequestHandler } from 'express';
import { UnsupportedMediaTypeError } from '../../utils/errors.utils';

export const MkcolMiddleware: RequestHandler = (req, _, next) => {
// if request content types are not 'application/xml', 'text/xml' or not defined, return 415
if (req.method === 'MKCOL') {
let contentType = req.headers['Content-Type'] ?? req.get('content-type');
if (contentType && contentType.length > 0) {
if (Array.isArray(contentType)) {
contentType = contentType[0];
}
contentType = contentType.toLowerCase().trim();

if (contentType !== 'application/xml' && contentType !== 'text/xml') {
throw new UnsupportedMediaTypeError('Unsupported Media Type');
}
}
// body must be empty
if (req.body && Object.keys(req.body).length > 0) {
throw new UnsupportedMediaTypeError('Unsupported Media Type');
}
}

next();
};
4 changes: 3 additions & 1 deletion src/webdav/webdav-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { MOVERequestHandler } from './handlers/MOVE.handler';
import { COPYRequestHandler } from './handlers/COPY.handler';
import { TrashService } from '../services/drive/trash.service';
import { Environment } from '@internxt/inxt-js';
import { MkcolMiddleware } from './middewares/mkcol.middleware';

export class WebDavServer {
constructor(
Expand Down Expand Up @@ -68,14 +69,15 @@ export class WebDavServer {
};

private readonly registerMiddlewares = async () => {
this.app.use(bodyParser.text({ type: ['application/xml', 'text/xml'] }));
this.app.use(ErrorHandlingMiddleware);
this.app.use(AuthMiddleware(AuthService.instance));
this.app.use(
RequestLoggerMiddleware({
enable: true,
}),
);
this.app.use(bodyParser.text({ type: ['application/xml', 'text/xml'] }));
this.app.use(MkcolMiddleware);
};

private readonly registerHandlers = async () => {
Expand Down
20 changes: 19 additions & 1 deletion test/utils/webdav.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { newFileItem, newFolderItem } from '../fixtures/drive.fixture';
import { DriveFolderService } from '../../src/services/drive/drive-folder.service';
import { DriveFileService } from '../../src/services/drive/drive-file.service';
import { fail } from 'node:assert';
import { NotFoundError } from '../../src/utils/errors.utils';
import { ConflictError, NotFoundError } from '../../src/utils/errors.utils';
import AppError from '@internxt/sdk/dist/shared/types/errors';

describe('Webdav utils', () => {
beforeEach(() => {
Expand Down Expand Up @@ -260,6 +261,23 @@ describe('Webdav utils', () => {
expect(findFileStub).not.toHaveBeenCalled();
});

it('When folder is not found, then it throws ConflictError', async () => {
const findFolderStub = vi
.spyOn(DriveFolderService.instance, 'getFolderMetadataByPath')
.mockRejectedValue(new AppError('Folder not found', 404));
const findFileStub = vi.spyOn(DriveFileService.instance, 'getFileMetadataByPath').mockRejectedValue(new Error());

try {
await WebDavUtils.getDriveItemFromResource(requestFolderFixture, DriveFolderService.instance, undefined);
fail('Expected function to throw an error, but it did not.');
} catch (error) {
expect(error).to.be.instanceOf(ConflictError);
}

expect(findFolderStub).toHaveBeenCalledOnce();
expect(findFileStub).not.toHaveBeenCalled();
});

it('When file resource is looked by its path, then it is returned', async () => {
const expectedFile = newFileItem();
const findFileStub = vi.spyOn(DriveFileService.instance, 'getFileMetadataByPath').mockResolvedValue(expectedFile);
Expand Down
5 changes: 5 additions & 0 deletions test/webdav/handlers/MKCOL.handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { newCreateFolderResponse, newFolderItem } from '../../fixtures/drive.fix
import { WebDavRequestedResource } from '../../../src/types/webdav.types';
import { WebDavUtils } from '../../../src/utils/webdav.utils';
import { DriveFolder } from '../../../src/services/database/drive-folder/drive-folder.domain';
import AppError from '@internxt/sdk/dist/shared/types/errors';

describe('MKCOL request handler', () => {
beforeEach(() => {
Expand Down Expand Up @@ -61,6 +62,9 @@ describe('MKCOL request handler', () => {
const createFolderStub = vi
.spyOn(driveFolderService, 'createFolder')
.mockReturnValue([Promise.resolve(newFolderResponse), { cancel: () => {} }]);
const getFolderMetadataStub = vi
.spyOn(driveFolderService, 'getFolderMetadataByPath')
.mockRejectedValue(new AppError('Folder not found', 404));
const createDatabaseFolderStub = vi
.spyOn(driveDatabaseManager, 'createFolder')
.mockResolvedValue({} as DriveFolder);
Expand All @@ -74,5 +78,6 @@ describe('MKCOL request handler', () => {
parentFolderUuid: parentFolder.uuid,
});
expect(createDatabaseFolderStub).toHaveBeenCalledOnce();
expect(getFolderMetadataStub).toHaveBeenCalledOnce();
});
});
4 changes: 2 additions & 2 deletions test/webdav/handlers/PUT.handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('PUT request handler', () => {
const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture);

await sut.handle(request, response);
expect(response.status).toHaveBeenCalledWith(200);
expect(response.status).toHaveBeenCalledWith(201);
expect(getRequestedResourceStub).toHaveBeenCalledTimes(2);
expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2);
expect(getAuthDetailsStub).toHaveBeenCalledOnce();
Expand Down Expand Up @@ -206,7 +206,7 @@ describe('PUT request handler', () => {
const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture);

await sut.handle(request, response);
expect(response.status).toHaveBeenCalledWith(200);
expect(response.status).toHaveBeenCalledWith(201);
expect(getRequestedResourceStub).toHaveBeenCalledTimes(2);
expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2);
expect(getAuthDetailsStub).toHaveBeenCalledOnce();
Expand Down
74 changes: 74 additions & 0 deletions test/webdav/middlewares/mkcol.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createWebDavRequestFixture, createWebDavResponseFixture } from '../../fixtures/webdav.fixture';
import { MkcolMiddleware } from '../../../src/webdav/middewares/mkcol.middleware';
import { fail } from 'node:assert';
import { UnsupportedMediaTypeError } from '../../../src/utils/errors.utils';

describe('MKCOL middleware', () => {
beforeEach(() => {
vi.restoreAllMocks();
});

it('When MKCOL content is application/xml, then it should call next', () => {
const req = createWebDavRequestFixture({
method: 'MKCOL',
url: '/anypath',
headers: { 'Content-Type': 'application/xml' },
});
const res = createWebDavResponseFixture({});
const next = vi.fn();

MkcolMiddleware(req, res, next);

expect(next).toHaveBeenCalledExactlyOnceWith();
});

it('When MKCOL content is text/xml, then it should call next', () => {
const req = createWebDavRequestFixture({
method: 'MKCOL',
url: '/anypath',
headers: [{ 'Content-Type': 'text/xml' }],
});
const res = createWebDavResponseFixture({});
const next = vi.fn();

MkcolMiddleware(req, res, next);

expect(next).toHaveBeenCalledExactlyOnceWith();
});

it('When MKCOL content is not XML, then it should call next with error', async () => {
const req = createWebDavRequestFixture({
method: 'MKCOL',
url: '/anypath',
headers: { 'Content-Type': 'application/json' },
});
const res = createWebDavResponseFixture({});
const next = vi.fn();

try {
await MkcolMiddleware(req, res, next);
fail('Expected function to throw an error, but it did not.');
} catch (error) {
expect(error).to.be.instanceOf(UnsupportedMediaTypeError);
}
});

it('When MKCOL has body content, then it should call next with error', async () => {
const req = createWebDavRequestFixture({
method: 'MKCOL',
url: '/anypath',
headers: { 'Content-Type': 'text/xml' },
body: '<?xml version="1.0" encoding="UTF-8"?><test></test>',
});
const res = createWebDavResponseFixture({});
const next = vi.fn();

try {
await MkcolMiddleware(req, res, next);
fail('Expected function to throw an error, but it did not.');
} catch (error) {
expect(error).to.be.instanceOf(UnsupportedMediaTypeError);
}
});
});
Loading