diff --git a/.eslintignore b/.eslintignore index cb74be8be1..c38d5744f5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1445,6 +1445,9 @@ packages/server/src/controllers/api/UserController.test.js.map packages/server/src/controllers/factory.d.ts packages/server/src/controllers/factory.js packages/server/src/controllers/factory.js.map +packages/server/src/controllers/index/FileController.d.ts +packages/server/src/controllers/index/FileController.js +packages/server/src/controllers/index/FileController.js.map packages/server/src/controllers/index/HomeController.d.ts packages/server/src/controllers/index/HomeController.js packages/server/src/controllers/index/HomeController.js.map @@ -1511,6 +1514,9 @@ packages/server/src/routes/api/sessions.js.map packages/server/src/routes/default.d.ts packages/server/src/routes/default.js packages/server/src/routes/default.js.map +packages/server/src/routes/index/files.d.ts +packages/server/src/routes/index/files.js +packages/server/src/routes/index/files.js.map packages/server/src/routes/index/home.d.ts packages/server/src/routes/index/home.js packages/server/src/routes/index/home.js.map diff --git a/.gitignore b/.gitignore index e8bd028bee..36b348dd0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1434,6 +1434,9 @@ packages/server/src/controllers/api/UserController.test.js.map packages/server/src/controllers/factory.d.ts packages/server/src/controllers/factory.js packages/server/src/controllers/factory.js.map +packages/server/src/controllers/index/FileController.d.ts +packages/server/src/controllers/index/FileController.js +packages/server/src/controllers/index/FileController.js.map packages/server/src/controllers/index/HomeController.d.ts packages/server/src/controllers/index/HomeController.js packages/server/src/controllers/index/HomeController.js.map @@ -1500,6 +1503,9 @@ packages/server/src/routes/api/sessions.js.map packages/server/src/routes/default.d.ts packages/server/src/routes/default.js packages/server/src/routes/default.js.map +packages/server/src/routes/index/files.d.ts +packages/server/src/routes/index/files.js +packages/server/src/routes/index/files.js.map packages/server/src/routes/index/home.d.ts packages/server/src/routes/index/home.js packages/server/src/routes/index/home.js.map diff --git a/packages/server/src/controllers/api/FileController.ts b/packages/server/src/controllers/api/FileController.ts index ffd4d3aaef..227973e752 100644 --- a/packages/server/src/controllers/api/FileController.ts +++ b/packages/server/src/controllers/api/FileController.ts @@ -58,10 +58,10 @@ export default class FileController extends BaseController { await this.putFileContent(sessionId, fileId, null); } - public async getChildren(sessionId: string, fileId: string, pagination: Pagination): Promise { + public async getChildren(sessionId: string, dirId: string, pagination: Pagination): Promise { const user = await this.initSession(sessionId); const fileModel = this.models.file({ userId: user.id }); - const parent: File = await fileModel.entityFromItemId(fileId); + const parent: File = await fileModel.entityFromItemId(dirId); return fileModel.toApiOutput(await fileModel.childrens(parent.id, pagination)); } diff --git a/packages/server/src/controllers/factory.ts b/packages/server/src/controllers/factory.ts index 3054e8622d..acd7559581 100644 --- a/packages/server/src/controllers/factory.ts +++ b/packages/server/src/controllers/factory.ts @@ -7,6 +7,7 @@ import IndexLoginController from './index/LoginController'; import IndexHomeController from './index/HomeController'; import IndexProfileController from './index/ProfileController'; import IndexUserController from './index/UserController'; +import IndexFileController from './index/FileController'; export class Controllers { @@ -48,6 +49,10 @@ export class Controllers { return new IndexUserController(this.models_); } + public indexFiles() { + return new IndexFileController(this.models_); + } + } export default function(models: Models) { diff --git a/packages/server/src/controllers/index/FileController.ts b/packages/server/src/controllers/index/FileController.ts new file mode 100644 index 0000000000..ca90356ad3 --- /dev/null +++ b/packages/server/src/controllers/index/FileController.ts @@ -0,0 +1,44 @@ +import BaseController from '../BaseController'; +import { View } from '../../services/MustacheService'; +import defaultView from '../../utils/defaultView'; +import { Pagination } from '../../models/utils/pagination'; +import { File } from '../../db'; + +export default class FileController extends BaseController { + + public async getIndex(sessionId: string, dirId: string, pagination: Pagination): Promise { + const owner = await this.initSession(sessionId); + const user = await this.initSession(sessionId); + const fileModel = this.models.file({ userId: user.id }); + const parent: File = dirId ? await fileModel.entityFromItemId(dirId) : await fileModel.userRootFile(); + const paginatedFiles = await fileModel.childrens(parent.id, pagination); + + const view: View = defaultView('files', owner); + view.content.paginatedFiles = paginatedFiles; + return view; + } + + // public async getOne(sessionId: string, isNew: boolean, userIdOrString: string | User = null, error: any = null): Promise { + // const owner = await this.initSession(sessionId); + // const userModel = this.models.user({ userId: owner.id }); + + // let user: User = {}; + + // if (typeof userIdOrString === 'string') { + // user = await userModel.load(userIdOrString as string); + // } else { + // user = userIdOrString as User; + // } + + // const view: View = defaultView('user', owner); + // view.content.user = user; + // view.content.isNew = isNew; + // view.content.buttonTitle = isNew ? 'Create user' : 'Update profile'; + // view.content.error = error; + // view.content.postUrl = `${baseUrl()}/users${isNew ? '/new' : `/${user.id}`}`; + // view.partials.push('errorBanner'); + + // return view; + // } + +} diff --git a/packages/server/src/models/FileModel.test.ts b/packages/server/src/models/FileModel.test.ts new file mode 100644 index 0000000000..1f9d4222da --- /dev/null +++ b/packages/server/src/models/FileModel.test.ts @@ -0,0 +1,56 @@ +import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, createFileTree } from '../utils/testUtils'; +import { File } from '../db'; + +describe('FileModel', function() { + + beforeAll(async () => { + await beforeAllDb('FileModel'); + }); + + afterAll(async () => { + await afterAllDb(); + }); + + beforeEach(async () => { + await beforeEachDb(); + }); + + test('should compute item full path', async function() { + const { user } = await createUserAndSession(1, true); + const fileModel = models().file({ userId: user.id }); + const rootId = await fileModel.userRootFileId(); + + const tree: any = { + folder1: {}, + folder2: { + file2_1: null, + file2_2: null, + }, + folder3: { + file3_1: null, + }, + file1: null, + file2: null, + file3: null, + }; + + await createFileTree(fileModel, rootId, tree); + + const testCases = Object.keys(tree) + .concat(Object.keys(tree.folder2)) + .concat(Object.keys(tree.folder3)); + + for (const t of testCases) { + const file: File = await fileModel.loadByName(t); + const path = await fileModel.itemFullPath(file); + const fileBack: File = await fileModel.entityFromItemId(path); + expect(file.id).toBe(fileBack.id); + } + + const rootPath = await fileModel.itemFullPath(await fileModel.userRootFile()); + expect(rootPath).toBe('root'); + const fileBack: File = await fileModel.entityFromItemId(rootPath); + expect(fileBack.id).toBe(rootId); + }); + +}); diff --git a/packages/server/src/models/FileModel.ts b/packages/server/src/models/FileModel.ts index c867a5c002..d7eb10a688 100644 --- a/packages/server/src/models/FileModel.ts +++ b/packages/server/src/models/FileModel.ts @@ -56,6 +56,17 @@ export default class FileModel extends BaseModel { return null; // Not a special dir } + public async itemFullPath(item: File): Promise { + const segments: string[] = []; + while (item) { + if (item.is_root) break; + segments.splice(0, 0, item.name); + item = item.parent_id ? await this.load(item.parent_id) : null; + } + + return segments.length ? (`root:/${segments.join('/')}:`) : 'root'; + } + public async entityFromItemId(idOrPath: string, options: EntityFromItemIdOptions = {}): Promise { options = { mustExist: true, ...options }; @@ -265,6 +276,19 @@ export default class FileModel extends BaseModel { return output; } + // Mostly makes sense for testing/debugging because the filename would + // have to globally unique, which is not a requirement. + public async loadByName(name: string): Promise { + const file: File = await this.db(this.tableName) + .select(this.defaultFields) + .where({ name: name }) + .andWhere({ owner_id: this.userId }) + .first(); + if (!file) throw new ErrorNotFound(`No such file: ${name}`); + await this.checkCanReadPermissions(file); + return file; + } + public async loadWithContent(id: string): Promise { const file: File = await this.db(this.tableName).select('*').where({ id: id }).first(); if (!file) return null; diff --git a/packages/server/src/routes/index/files.ts b/packages/server/src/routes/index/files.ts new file mode 100644 index 0000000000..b306d8fc7f --- /dev/null +++ b/packages/server/src/routes/index/files.ts @@ -0,0 +1,21 @@ +import { SubPath, Route } from '../../utils/routeUtils'; +import { AppContext } from '../../utils/types'; +import { contextSessionId } from '../../utils/requestUtils'; +import { ErrorMethodNotAllowed } from '../../utils/errors'; +import { requestPagination } from '../../models/utils/pagination'; + +const route: Route = { + + exec: async function(path: SubPath, ctx: AppContext) { + const sessionId = contextSessionId(ctx); + + if (ctx.method === 'GET') { + return ctx.controllers.indexFiles().getIndex(sessionId, path.id, requestPagination(ctx.query)); + } + + throw new ErrorMethodNotAllowed(); + }, + +}; + +export default route; diff --git a/packages/server/src/routes/routes.ts b/packages/server/src/routes/routes.ts index 73d769e923..58f1d11752 100644 --- a/packages/server/src/routes/routes.ts +++ b/packages/server/src/routes/routes.ts @@ -3,20 +3,19 @@ import { Routes } from '../utils/routeUtils'; import apiSessions from './api/sessions'; import apiPing from './api/ping'; import apiFiles from './api/files'; -// import oauth2Authorize from './oauth2/authorize'; import indexLoginRoute from './index/login'; import indexLogoutRoute from './index/logout'; import indexHomeRoute from './index/home'; import indexProfileRoute from './index/profile'; import indexUsersRoute from './index/users'; import indexUserRoute from './index/user'; +import indexFilesRoute from './index/files'; import defaultRoute from './default'; const routes: Routes = { 'api/ping': apiPing, 'api/sessions': apiSessions, 'api/files': apiFiles, - // 'oauth2/authorize': oauth2Authorize, 'login': indexLoginRoute, 'logout': indexLogoutRoute, @@ -24,6 +23,7 @@ const routes: Routes = { 'profile': indexProfileRoute, 'users': indexUsersRoute, 'user': indexUserRoute, + 'files': indexFilesRoute, '': defaultRoute, }; diff --git a/packages/server/src/utils/testUtils.ts b/packages/server/src/utils/testUtils.ts index 6fbd252962..6665cf6008 100644 --- a/packages/server/src/utils/testUtils.ts +++ b/packages/server/src/utils/testUtils.ts @@ -1,10 +1,11 @@ -import { User, Session, DbConnection, connectDb, disconnectDb } from '../db'; +import { User, Session, DbConnection, connectDb, disconnectDb, File } from '../db'; import { createDb } from '../tools/dbTools'; import modelFactory from '../models/factory'; import controllerFactory from '../controllers/factory'; import baseConfig from '../config-tests'; import { Config } from './types'; import { initConfig } from '../config'; +import FileModel from '../models/FileModel'; // Takes into account the fact that this file will be inside the /dist directory // when it runs. @@ -81,6 +82,20 @@ export const createUser = async function(index: number = 1, isAdmin: boolean = f return models().user().save({ email: `user${index}@localhost`, password: '123456', is_admin: isAdmin ? 1 : 0 }, { skipValidation: true }); }; +export async function createFileTree(fileModel: FileModel, parentId: string, tree: any): Promise { + for (const name in tree) { + const children: any = tree[name]; + const isDir = children !== null; + const newFile: File = await fileModel.save({ + parent_id: parentId, + name: name, + is_directory: isDir ? 1 : 0, + }); + + if (isDir && Object.keys(children).length) await createFileTree(fileModel, newFile.id, children); + } +} + export async function checkThrowAsync(asyncFn: Function): Promise { try { await asyncFn(); diff --git a/packages/server/src/views/index/files.mustache b/packages/server/src/views/index/files.mustache new file mode 100644 index 0000000000..6f0ebcc86a --- /dev/null +++ b/packages/server/src/views/index/files.mustache @@ -0,0 +1,5 @@ +
+ {{#paginatedFiles.items}} +
{{name}}
+ {{/paginatedFiles.items}} +
\ No newline at end of file diff --git a/packages/server/src/views/partials/navbar.mustache b/packages/server/src/views/partials/navbar.mustache index 8e1aabe132..776d2ad828 100644 --- a/packages/server/src/views/partials/navbar.mustache +++ b/packages/server/src/views/partials/navbar.mustache @@ -8,6 +8,7 @@