Skip to content

Commit

Permalink
feat: Build the file path with the root in the service
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Burnes <[email protected]>
  • Loading branch information
apburnes committed Feb 12, 2025
1 parent dbbaa81 commit ebc2984
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 47 deletions.
2 changes: 1 addition & 1 deletion api/controllers/file-storage-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ module.exports = wrapHandlers({
async listDirectoryFiles(req, res) {
const { params, query, user } = req;
const {
path = '~assets/',
path = '',
limit = 50,
page = 1,
sortKey = 'updatedAt',
Expand Down
60 changes: 28 additions & 32 deletions api/services/file-storage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ class SiteFileStorageSerivce {
this.userId = userId;

this.initialized = null;
this.fileStorageServiceId = null;
}

async createClient() {
Expand Down Expand Up @@ -95,30 +94,28 @@ class SiteFileStorageSerivce {
}

async createAssetRoot() {
this._isInitialized();
this.#isInitialized();

return this.s3Client.putObject('', this.S3_BASE_PATH);
}

async createDirectory(parent, name) {
await this._getSiteFileStorageService();

const directoryName = slugify(name);
const directoryPath = path.join(this.S3_BASE_PATH, parent, directoryName, '/');
const directoryPath = this.#buildKeyPath(`${parent}/${directoryName}`);

await this.s3Client.putObject('', directoryPath);

const fsf = await FileStorageFile.create({
name,
key: directoryPath,
type: 'directory',
fileStorageServiceId: this.fileStorageServiceId,
fileStorageServiceId: this.id,
description: 'directory',
});

await FileStorageUserAction.create({
userId: this.userId,
fileStorageServiceId: this.fileStorageServiceId,
fileStorageServiceId: this.id,
fileStorageFileId: fsf.id,
method: FileStorageUserAction.METHODS.POST,
description: FileStorageUserAction.ACTION_TYPES.CREATE_DIRECTORY,
Expand All @@ -128,7 +125,7 @@ class SiteFileStorageSerivce {
}

async createFileStorageService() {
this._isInitialized();
this.#isInitialized();

await this.createAssetRoot();

Expand All @@ -140,7 +137,7 @@ class SiteFileStorageSerivce {
serviceName: this.serviceInstance.name,
});

this.fileStorageServiceId = fss.id;
this.id = fss.id;

return fss;
}
Expand Down Expand Up @@ -220,7 +217,8 @@ class SiteFileStorageSerivce {
directory,
{ limit = 50, page = 1, order = [['name', 'ASC']] } = {},
) {
const dir = normalizeDirectoryPath(directory);
const key = this.#buildKeyPath(directory);

const results = await paginate(
FileStorageFile,
serializeFileStorageFiles,
Expand All @@ -232,9 +230,9 @@ class SiteFileStorageSerivce {
where: {
fileStorageServiceId: this.id,
[Op.and]: [
{ key: { [Op.like]: `${dir}%` } },
{ key: { [Op.notLike]: `${dir}%/_%` } },
{ key: { [Op.ne]: dir } },
{ key: { [Op.like]: `${key}%` } },
{ key: { [Op.notLike]: `${key}%/_%` } },
{ key: { [Op.ne]: key } },
],
},
order,
Expand All @@ -245,24 +243,23 @@ class SiteFileStorageSerivce {
}

async uploadFile(name, fileBuffer, type, parent, metadata = {}) {
await this._getSiteFileStorageService();

const filename = slugify(name);
const directoryPath = path.join(this.S3_BASE_PATH, parent, filename);
const directoryPath = this.#buildKeyPath(parent);
const key = path.join(directoryPath, filename);

await this.s3Client.putObject(fileBuffer, directoryPath);
await this.s3Client.putObject(fileBuffer, key);

const fsf = await FileStorageFile.create({
name,
key: directoryPath,
key,
type: type,
metadata,
fileStorageServiceId: this.fileStorageServiceId,
fileStorageServiceId: this.id,
});

await FileStorageUserAction.create({
userId: this.userId,
fileStorageServiceId: this.fileStorageServiceId,
fileStorageServiceId: this.id,
fileStorageFileId: fsf.id,
method: FileStorageUserAction.METHODS.POST,
description: FileStorageUserAction.ACTION_TYPES.UPLOAD_FILE,
Expand All @@ -271,22 +268,21 @@ class SiteFileStorageSerivce {
return fsf;
}

_isInitialized() {
if (!this.initialized) {
throw Error('Initialize the class instance with `await instance.createClient()`');
}
}
#buildKeyPath(keyPath) {
const normalized = normalizeDirectoryPath(keyPath);
const root = normalized.split('/').filter((x) => x)[0];

async _getSiteFileStorageService() {
this._isInitialized();
if (`${root}/` !== this.S3_BASE_PATH) {
return path.join(this.S3_BASE_PATH, normalized);
}

const fss = await FileStorageService.findByPk(this.id);
return normalized;
}

if (!fss) {
throw Error('Site file service must be created.');
#isInitialized() {
if (!this.initialized) {
throw Error('Initialize the class instance with `await instance.createClient()`');
}

this.fileStorageServiceId = fss.id;
}
}

Expand Down
116 changes: 102 additions & 14 deletions test/api/unit/services/FileStorage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('FileStorage services', () => {
const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.fileStorageServiceId,
fileStorageServiceId: client.id,
userId: user.id,
},
});
Expand All @@ -119,6 +119,30 @@ describe('FileStorage services', () => {
);
});

it('should prepend the root directory if not provided', async () => {
const { client, user } = await createFileStorageServiceClient();
const basepath = '/a/b/c';
const name = 'another-directory';
const key = path.join(client.S3_BASE_PATH, basepath, name);
const s3stub = sinon.stub(S3Helper.S3Client.prototype, 'putObject').resolves();

const results = await client.createDirectory(basepath, name);

const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.id,
userId: user.id,
},
});

expect(s3stub.calledOnceWith('', `${key}/`)).to.be.eq(true);
expect(fsua.method).to.be.eq(FileStorageUserAction.METHODS.POST);
expect(fsua.description).to.be.eq(
FileStorageUserAction.ACTION_TYPES.CREATE_DIRECTORY,
);
});

it('should append trailing slash if not provided', async () => {
const { client, user } = await createFileStorageServiceClient();
const basepath = '/a/b/c';
Expand All @@ -131,7 +155,7 @@ describe('FileStorage services', () => {
const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.fileStorageServiceId,
fileStorageServiceId: client.id,
userId: user.id,
},
});
Expand All @@ -155,7 +179,7 @@ describe('FileStorage services', () => {
const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.fileStorageServiceId,
fileStorageServiceId: client.id,
userId: user.id,
},
});
Expand All @@ -180,7 +204,7 @@ describe('FileStorage services', () => {
const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.fileStorageServiceId,
fileStorageServiceId: client.id,
userId: user.id,
},
});
Expand Down Expand Up @@ -229,7 +253,7 @@ describe('FileStorage services', () => {

const fsua = await FileStorageUserAction.findAll({
where: {
fileStorageServiceId: client.fileStorageServiceId,
fileStorageServiceId: client.id,
userId: user.id,
},
});
Expand Down Expand Up @@ -452,10 +476,10 @@ describe('FileStorage services', () => {
});
});

describe('listDirectoryFiles', () => {
describe('.listDirectoryFiles', () => {
it('should list files in a directory', async () => {
const { client, fss } = await createFileStorageServiceClient();
const dir = 'a/b/c/';
const dir = path.join(client.S3_BASE_PATH, 'a/b/c/');
const subdir = `${dir}/d/`;
const expectedList = await factory.fileStorageFile.createBulk(fss.id, dir, {
files: 10,
Expand All @@ -482,10 +506,49 @@ describe('FileStorage services', () => {
expect(files.length).to.be.eq(allFileCount);
});

it('should list files in a directory and not the parent directory', async () => {
it('should list files in and prepend root directory', async () => {
const { client, fss } = await createFileStorageServiceClient();
const dir = 'a/b/c/';
const subdir = `${dir}/d/`;
const expectedList = await factory.fileStorageFile.createBulk(
fss.id,
// The prepended root directory
`${client.S3_BASE_PATH}${dir}`,
{
files: 10,
directories: 2,
},
);
const expectedCount = expectedList.files.length + expectedList.directories.length;
const unexpectedList = await factory.fileStorageFile.createBulk(
fss.id,
// The prepended root directory
`${client.S3_BASE_PATH}${subdir}`,
{
files: 2,
directories: 1,
},
);
const unexpectedCount =
unexpectedList.files.length + unexpectedList.directories.length;
const allFileCount = expectedCount + unexpectedCount;
const results = await client.listDirectoryFiles(dir, { limit: 100 });

const files = await FileStorageFile.findAll({
where: { fileStorageServiceId: fss.id },
});

expect(results.currentPage).to.be.eq(1);
expect(results.totalPages).to.be.eq(1);
expect(results.data.length).to.be.eq(expectedCount);
expect(results.totalItems).to.be.eq(expectedCount);
expect(files.length).to.be.eq(allFileCount);
});

it('should list files in a directory and not the parent directory', async () => {
const { client, fss } = await createFileStorageServiceClient();
const dir = path.join(client.S3_BASE_PATH, 'a/b/c/');
const subdir = `${dir}/d/`;
await factory.fileStorageFile.create({
fileStorageServiceId: fss.id,
type: 'directory',
Expand Down Expand Up @@ -518,7 +581,7 @@ describe('FileStorage services', () => {

it('should list files for directory on multiple pages', async () => {
const { client, fss } = await createFileStorageServiceClient();
const dir = 'a/b/c/';
const dir = path.join(client.S3_BASE_PATH, 'a/b/c/');
const subdir = `${dir}/d/`;
const limit = 2;
const expectedList = await factory.fileStorageFile.createBulk(fss.id, dir, {
Expand Down Expand Up @@ -550,7 +613,7 @@ describe('FileStorage services', () => {

it('should list files for directory from second page', async () => {
const { client, fss } = await createFileStorageServiceClient();
const dir = 'a/b/c/';
const dir = path.join(client.S3_BASE_PATH, 'a/b/c/');
const subdir = `${dir}/d/`;
const limit = 2;
const page = 2;
Expand Down Expand Up @@ -583,7 +646,7 @@ describe('FileStorage services', () => {

it('should sort by name desc', async () => {
const { client, fss } = await createFileStorageServiceClient();
const dir = 'a/b/c/';
const dir = path.join(client.S3_BASE_PATH, 'a/b/c/');
const subdir = `${dir}/d/`;
const order = [['name', 'desc']];
const expectedList = await factory.fileStorageFile.createBulk(fss.id, dir, {
Expand Down Expand Up @@ -630,20 +693,45 @@ describe('FileStorage services', () => {
const fileBuffer = Buffer.from('file content');
const type = 'plain/txt';
const metadata = { size: 123 };
const key = path.join(client.S3_BASE_PATH, parent, name);
const expectedKey = path.join(client.S3_BASE_PATH, parent, name);
const s3stub = sinon.stub(S3Helper.S3Client.prototype, 'putObject').resolves();

const results = await client.uploadFile(name, fileBuffer, type, parent, metadata);

const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.id,
userId: user.id,
},
});

expect(s3stub.calledOnceWith(fileBuffer, `${expectedKey}`)).to.be.eq(true);
expect(fsua.method).to.be.eq(FileStorageUserAction.METHODS.POST);
expect(fsua.description).to.be.eq(FileStorageUserAction.ACTION_TYPES.UPLOAD_FILE);
});

it('should create a directory appended to the ~assets root', async () => {
const { client, user } = await createFileStorageServiceClient();
const parent = '/a/b/c';
const name = 'test.txt';
const fileBuffer = Buffer.from('file content');
const type = 'plain/txt';
const metadata = { size: 123 };
const expectedKey = path.join(client.S3_BASE_PATH, parent, name);
const s3stub = sinon.stub(S3Helper.S3Client.prototype, 'putObject').resolves();

const results = await client.uploadFile(name, fileBuffer, type, parent, metadata);

const fsua = await FileStorageUserAction.findOne({
where: {
fileStorageFileId: results.id,
fileStorageServiceId: client.fileStorageServiceId,
fileStorageServiceId: client.id,
userId: user.id,
},
});

expect(s3stub.calledOnceWith(fileBuffer, `${key}`)).to.be.eq(true);
expect(s3stub.calledOnceWith(fileBuffer, `${expectedKey}`)).to.be.eq(true);
expect(fsua.method).to.be.eq(FileStorageUserAction.METHODS.POST);
expect(fsua.description).to.be.eq(FileStorageUserAction.ACTION_TYPES.UPLOAD_FILE);
});
Expand Down

0 comments on commit ebc2984

Please sign in to comment.