1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Laurent Cozic
8801e82cab pagination 2020-12-29 14:35:51 +00:00
Laurent Cozic
c4757d6c60 pagination 2020-12-29 00:13:04 +00:00
Laurent Cozic
af6c79b844 file manager 2020-12-28 17:24:06 +00:00
22 changed files with 343 additions and 74 deletions

View File

@@ -1481,6 +1481,9 @@ packages/server/src/models/ChangeModel.test.js.map
packages/server/src/models/FileModel.d.ts packages/server/src/models/FileModel.d.ts
packages/server/src/models/FileModel.js packages/server/src/models/FileModel.js
packages/server/src/models/FileModel.js.map packages/server/src/models/FileModel.js.map
packages/server/src/models/FileModel.test.d.ts
packages/server/src/models/FileModel.test.js
packages/server/src/models/FileModel.test.js.map
packages/server/src/models/PermissionModel.d.ts packages/server/src/models/PermissionModel.d.ts
packages/server/src/models/PermissionModel.js packages/server/src/models/PermissionModel.js
packages/server/src/models/PermissionModel.js.map packages/server/src/models/PermissionModel.js.map

3
.gitignore vendored
View File

@@ -1470,6 +1470,9 @@ packages/server/src/models/ChangeModel.test.js.map
packages/server/src/models/FileModel.d.ts packages/server/src/models/FileModel.d.ts
packages/server/src/models/FileModel.js packages/server/src/models/FileModel.js
packages/server/src/models/FileModel.js.map packages/server/src/models/FileModel.js.map
packages/server/src/models/FileModel.test.d.ts
packages/server/src/models/FileModel.test.js
packages/server/src/models/FileModel.test.js.map
packages/server/src/models/PermissionModel.d.ts packages/server/src/models/PermissionModel.d.ts
packages/server/src/models/PermissionModel.js packages/server/src/models/PermissionModel.js
packages/server/src/models/PermissionModel.js.map packages/server/src/models/PermissionModel.js.map

View File

@@ -429,6 +429,11 @@
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
}, },
"@fortawesome/fontawesome-free": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
},
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1552,6 +1557,14 @@
"dev": true, "dev": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
},
"dependencies": {
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
}
} }
}, },
"arr-diff": { "arr-diff": {
@@ -2439,6 +2452,11 @@
"whatwg-url": "^8.0.0" "whatwg-url": "^8.0.0"
} }
}, },
"dayjs": {
"version": "1.9.8",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.8.tgz",
"integrity": "sha512-F42qBtJRa30FKF7XDnOQyNUTsaxDkuaZRj/i7BejSHC34LlLfPoIU4aeopvWfM+m1dJ6/DHKAWLg2ur+pLgq1w=="
},
"debug": { "debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -7160,10 +7178,9 @@
} }
}, },
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
"dev": true
}, },
"sqlite3": { "sqlite3": {
"version": "4.1.0", "version": "4.1.0",

View File

@@ -12,10 +12,12 @@
"watch": "tsc --watch --project tsconfig.json" "watch": "tsc --watch --project tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@joplin/lib": "^1.0.9", "@joplin/lib": "^1.0.9",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bulma": "^0.9.1", "bulma": "^0.9.1",
"bulma-prefers-dark": "^0.1.0-beta.0", "bulma-prefers-dark": "^0.1.0-beta.0",
"dayjs": "^1.9.8",
"formidable": "^1.2.2", "formidable": "^1.2.2",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"html-entities": "^1.3.1", "html-entities": "^1.3.1",

View File

@@ -25,3 +25,11 @@ input.form-control {
.main { .main {
padding: 0 3rem; padding: 0 3rem;
} }
table.table .nowrap {
white-space: nowrap;
}
table.table .stretch {
width: 100%;
}

View File

@@ -71,11 +71,11 @@ app.use(async (ctx: Koa.Context) => {
} }
} catch (error) { } catch (error) {
if (error.httpCode >= 400 && error.httpCode < 500) { if (error.httpCode >= 400 && error.httpCode < 500) {
appLogger().error(error.httpCode + ': ' + `${ctx.request.method} ${ctx.path}` + ' : ' + error.message); appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
} else { } else {
appLogger().error(error); appLogger().error(error);
} }
ctx.response.status = error.httpCode ? error.httpCode : 500; ctx.response.status = error.httpCode ? error.httpCode : 500;
const responseFormat = routeResponseFormat(match, ctx.path); const responseFormat = routeResponseFormat(match, ctx.path);
@@ -87,6 +87,7 @@ app.use(async (ctx: Koa.Context) => {
path: 'index/error', path: 'index/error',
content: { content: {
error, error,
stack: env === 'dev' ? error.stack : '',
}, },
}; };
ctx.response.body = await mustacheService.renderView(view); ctx.response.body = await mustacheService.renderView(view);
@@ -111,8 +112,8 @@ async function main() {
const globalLogger = new Logger(); const globalLogger = new Logger();
// globalLogger.addTarget(TargetType.File, { path: `${config().logDir}/app.txt` }); // globalLogger.addTarget(TargetType.File, { path: `${config().logDir}/app.txt` });
globalLogger.addTarget(TargetType.Console, { globalLogger.addTarget(TargetType.Console, {
format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s', format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s',
formatInfo: '%(date_time)s: %(prefix)s: %(message)s', formatInfo: '%(date_time)s: %(prefix)s: %(message)s',
}); });
Logger.initializeGlobalLogger(globalLogger); Logger.initializeGlobalLogger(globalLogger);

View File

@@ -3,20 +3,20 @@ import configBase from './config-base';
const config: Config = { const config: Config = {
...configBase, ...configBase,
// database: {
// name: 'dev',
// client: 'sqlite3',
// asyncStackTraces: true,
// },
database: { database: {
client: 'pg', name: 'dev',
name: 'joplin', client: 'sqlite3',
user: 'joplin',
host: 'localhost',
port: 5432,
password: 'joplin',
asyncStackTraces: true, asyncStackTraces: true,
}, },
// database: {
// client: 'pg',
// name: 'joplin',
// user: 'joplin',
// host: 'localhost',
// port: 5432,
// password: 'joplin',
// asyncStackTraces: true,
// },
}; };
export default config; export default config;

View File

@@ -1,44 +1,85 @@
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { View } from '../../services/MustacheService'; import { View } from '../../services/MustacheService';
import defaultView from '../../utils/defaultView'; import defaultView from '../../utils/defaultView';
import { Pagination } from '../../models/utils/pagination'; import { Pagination, pageMaxSize, PaginationOrder, requestPaginationOrder, PaginationOrderDir, validatePagination, createPaginationLinks } from '../../models/utils/pagination';
import { File } from '../../db'; import { File } from '../../db';
import { baseUrl } from '../../config';
import { formatDateTime } from '../../utils/time';
import { setQueryParameters } from '../../utils/urlUtils';
export function makeFilePagination(query: any): Pagination {
const limit = Number(query.limit) || pageMaxSize;
const order: PaginationOrder[] = requestPaginationOrder(query, 'name', PaginationOrderDir.ASC);
order.splice(0, 0, { by: 'is_directory', dir: PaginationOrderDir.DESC });
const page: number = 'page' in query ? Number(query.page) : 1;
const output: Pagination = { limit, order, page };
validatePagination(output);
return output;
}
export default class FileController extends BaseController { export default class FileController extends BaseController {
public async getIndex(sessionId: string, dirId: string, pagination: Pagination): Promise<View> { public async getIndex(sessionId: string, dirId: string, query: any): Promise<View> {
// Query parameters that should be appended to pagination-related URLs
const baseUrlQuery: any = {};
if (query.limit) baseUrlQuery.limit = query.limit;
if (query.order_by) baseUrlQuery.order_by = query.order_by;
if (query.order_dir) baseUrlQuery.order_dir = query.order_dir;
const pagination = makeFilePagination(query);
const owner = await this.initSession(sessionId); const owner = await this.initSession(sessionId);
const user = await this.initSession(sessionId); const user = await this.initSession(sessionId);
const fileModel = this.models.file({ userId: user.id }); const fileModel = this.models.file({ userId: user.id });
const parent: File = dirId ? await fileModel.entityFromItemId(dirId) : await fileModel.userRootFile(); const root = await fileModel.userRootFile();
const parentTemp: File = dirId ? await fileModel.entityFromItemId(dirId) : root;
const parent: File = await fileModel.load(parentTemp.id);
const paginatedFiles = await fileModel.childrens(parent.id, pagination); const paginatedFiles = await fileModel.childrens(parent.id, pagination);
const pageCount = Math.ceil((await fileModel.childrenCount(parent.id)) / pagination.limit);
const parentBaseUrl = `${baseUrl()}/files/${await fileModel.itemFullPath(parent)}`;
const paginationLinks = createPaginationLinks(pagination.page, pageCount, setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
async function fileToViewItem(file: File): Promise<any> {
const filePath = await fileModel.itemFullPath(file);
let url = `${baseUrl()}/files/${filePath}`;
if (!file.is_directory) {
url += '/content';
} else {
url = setQueryParameters(url, baseUrlQuery);
}
return {
name: file.name,
url,
type: file.is_directory ? 'directory' : 'file',
icon: file.is_directory ? 'far fa-folder' : 'far fa-file',
timestamp: formatDateTime(file.updated_time),
mime: !file.is_directory ? (file.mime_type || 'binary') : '',
};
}
const files: any[] = [];
if (parent.id !== root.id) {
const p = await fileModel.load(parent.parent_id);
files.push({
...await fileToViewItem(p),
icon: 'fas fa-arrow-left',
name: '..',
});
}
for (const file of paginatedFiles.items) {
files.push(await fileToViewItem(file));
}
const view: View = defaultView('files', owner); const view: View = defaultView('files', owner);
view.content.paginatedFiles = paginatedFiles; view.content.paginatedFiles = { ...paginatedFiles, items: files };
view.content.paginationLinks = paginationLinks;
view.cssFiles = ['index/files'];
view.partials.push('pagination');
return view; return view;
} }
// public async getOne(sessionId: string, isNew: boolean, userIdOrString: string | User = null, error: any = null): Promise<View> {
// 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;
// }
} }

View File

@@ -332,6 +332,13 @@ export default class FileModel extends BaseModel {
return super.save(file, options); return super.save(file, options);
} }
public async childrenCount(id: string): Promise<number> {
const parent = await this.load(id);
await this.checkCanReadPermissions(parent);
const r = await this.db(this.tableName).select('id').where('parent_id', id).count('id', { as: 'total' });
return r.length && r[0].total ? r[0].total : 0;
}
public async childrens(id: string, pagination: Pagination): Promise<PaginatedFiles> { public async childrens(id: string, pagination: Pagination): Promise<PaginatedFiles> {
const parent = await this.load(id); const parent = await this.load(id);
await this.checkCanReadPermissions(parent); await this.checkCanReadPermissions(parent);

View File

@@ -1,5 +1,5 @@
import { expectThrow } from '../../utils/testUtils'; import { expectThrow } from '../../utils/testUtils';
import { defaultPagination, Pagination, requestPagination } from './pagination'; import { defaultPagination, Pagination, createPaginationLinks, requestPagination } from './pagination';
describe('pagination', function() { describe('pagination', function() {
@@ -69,4 +69,56 @@ describe('pagination', function() {
await expectThrow(async () => requestPagination({ page: 0 })); await expectThrow(async () => requestPagination({ page: 0 }));
}); });
test('should create page link logic', async function() {
expect(createPaginationLinks(1, 5)).toEqual([
{ page: 1, isCurrent: true },
{ page: 2 },
{ page: 3 },
{ page: 4 },
{ page: 5 },
]);
expect(createPaginationLinks(3, 5)).toEqual([
{ page: 1 },
{ page: 2 },
{ page: 3, isCurrent: true },
{ page: 4 },
{ page: 5 },
]);
expect(createPaginationLinks(1, 10)).toEqual([
{ page: 1, isCurrent: true },
{ page: 2 },
{ page: 3 },
{ page: 4 },
{ page: 5 },
{ isEllipsis: true },
{ page: 9 },
{ page: 10 },
]);
expect(createPaginationLinks(10, 20)).toEqual([
{ page: 1 },
{ page: 2 },
{ isEllipsis: true },
{ page: 8 },
{ page: 9 },
{ page: 10, isCurrent: true },
{ page: 11 },
{ page: 12 },
{ isEllipsis: true },
{ page: 19 },
{ page: 20 },
]);
expect(createPaginationLinks(20, 20)).toEqual([
{ page: 1 },
{ page: 2 },
{ isEllipsis: true },
{ page: 18 },
{ page: 19 },
{ page: 20, isCurrent: true },
]);
});
}); });

View File

@@ -26,17 +26,17 @@ export interface PaginatedResults {
cursor?: string; cursor?: string;
} }
const pageMaxSize = 1000; export const pageMaxSize = 1000;
const defaultOrderField = 'updated_time'; const defaultOrderField_ = 'updated_time';
const defaultOrderDir = PaginationOrderDir.DESC; const defaultOrderDir_ = PaginationOrderDir.DESC;
export function defaultPagination(): Pagination { export function defaultPagination(): Pagination {
return { return {
limit: pageMaxSize, limit: pageMaxSize,
order: [ order: [
{ {
by: defaultOrderField, by: defaultOrderField_,
dir: defaultOrderDir, dir: defaultOrderDir_,
}, },
], ],
page: 1, page: 1,
@@ -47,7 +47,10 @@ function dbOffset(pagination: Pagination): number {
return pagination.limit * (pagination.page - 1); return pagination.limit * (pagination.page - 1);
} }
function requestPaginationOrder(query: any): PaginationOrder[] { export function requestPaginationOrder(query: any, defaultOrderField: string = null, defaultOrderDir: PaginationOrderDir = null): PaginationOrder[] {
if (defaultOrderField === null) defaultOrderField = defaultOrderField_;
if (defaultOrderDir === null) defaultOrderDir = defaultOrderDir_;
const orderBy: string = 'order_by' in query ? query.order_by : defaultOrderField; const orderBy: string = 'order_by' in query ? query.order_by : defaultOrderField;
const orderDir: PaginationOrderDir = 'order_dir' in query ? query.order_dir : defaultOrderDir; const orderDir: PaginationOrderDir = 'order_dir' in query ? query.order_dir : defaultOrderDir;
@@ -59,7 +62,7 @@ function requestPaginationOrder(query: any): PaginationOrder[] {
}]; }];
} }
function validatePagination(p: Pagination): Pagination { export function validatePagination(p: Pagination): Pagination {
if (p.limit < 0 || p.limit > pageMaxSize) throw new ErrorBadRequest(`Limit out of bond: ${p.limit}`); if (p.limit < 0 || p.limit > pageMaxSize) throw new ErrorBadRequest(`Limit out of bond: ${p.limit}`);
if (p.page <= 0) throw new ErrorBadRequest(`Invalid page number: ${p.page}`); if (p.page <= 0) throw new ErrorBadRequest(`Invalid page number: ${p.page}`);
@@ -104,11 +107,74 @@ export function requestChangePagination(query: any): ChangePagination {
return output; return output;
} }
export interface PageLink {
page?: number;
isEllipsis?: boolean;
isCurrent?: boolean;
url?: string;
}
export function createPaginationLinks(page: number, pageCount: number, urlTemplate: string = null): PageLink[] {
if (!pageCount) return [];
let output: PageLink[] = [];
const firstPage: number = Math.max(page - 2, 1);
for (let p = firstPage; p <= firstPage + 4; p++) {
if (p > pageCount) break;
output.push({ page: p });
}
const firstPages: PageLink[] = [];
for (let p = 1; p <= 2; p++) {
if (output.find(o => o.page === p) || p > pageCount) continue;
firstPages.push({ page: p });
}
if (firstPages.length && (output[0].page - firstPages[firstPages.length - 1].page) > 1) {
firstPages.push({ isEllipsis: true });
}
output = firstPages.concat(output);
const lastPages: PageLink[] = [];
for (let p = pageCount - 1; p <= pageCount; p++) {
if (output.find(o => o.page === p) || p > pageCount || p < 1) continue;
lastPages.push({ page: p });
}
if (lastPages.length && (lastPages[0].page - output[output.length - 1].page) > 1) {
output.push({ isEllipsis: true });
}
output = output.concat(lastPages);
output = output.map(o => {
return o.page === page ? { ...o, isCurrent: true } : o;
});
if (urlTemplate) {
output = output.map(o => {
if (o.isEllipsis) return o;
return { ...o, url: urlTemplate.replace(/PAGE_NUMBER/, o.page.toString()) };
});
}
return output;
}
export async function paginateDbQuery(query: Knex.QueryBuilder, pagination: Pagination): Promise<PaginatedResults> { export async function paginateDbQuery(query: Knex.QueryBuilder, pagination: Pagination): Promise<PaginatedResults> {
pagination = processCursor(pagination); pagination = processCursor(pagination);
const orderSql: any[] = pagination.order.map(o => {
return {
column: o.by,
order: o.dir,
};
});
const items = await query const items = await query
.orderBy(pagination.order[0].by, pagination.order[0].dir) .orderBy(orderSql)
.offset(dbOffset(pagination)) .offset(dbOffset(pagination))
.limit(pagination.limit); .limit(pagination.limit);

View File

@@ -1,7 +1,7 @@
import { ErrorNotFound, ErrorMethodNotAllowed, ErrorBadRequest } from '../../utils/errors'; import { ErrorNotFound, ErrorMethodNotAllowed, ErrorBadRequest } from '../../utils/errors';
import { File } from '../../db'; import { File } from '../../db';
import { bodyFields, formParse, headerSessionId } from '../../utils/requestUtils'; import { bodyFields, formParse, headerSessionId } from '../../utils/requestUtils';
import { SubPath, Route, ResponseType, Response } from '../../utils/routeUtils'; import { SubPath, Route, respondWithFileContent } from '../../utils/routeUtils';
import { AppContext } from '../../utils/types'; import { AppContext } from '../../utils/types';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import { requestChangePagination, requestPagination } from '../../models/utils/pagination'; import { requestChangePagination, requestPagination } from '../../models/utils/pagination';
@@ -31,12 +31,8 @@ const route: Route = {
if (path.link === 'content') { if (path.link === 'content') {
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
const koaResponse = ctx.response;
const file: File = await fileController.getFileContent(headerSessionId(ctx.headers), path.id); const file: File = await fileController.getFileContent(headerSessionId(ctx.headers), path.id);
koaResponse.body = file.content; return respondWithFileContent(ctx.response, file);
koaResponse.set('Content-Type', file.mime_type);
koaResponse.set('Content-Length', file.size.toString());
return new Response(ResponseType.KoaResponse, koaResponse);
} }
if (ctx.method === 'PUT') { if (ctx.method === 'PUT') {

View File

@@ -13,14 +13,17 @@ interface PathToFileMap {
} }
// Most static assets should be in /public, but for those that are not, for // Most static assets should be in /public, but for those that are not, for
// example if they are in node_modules, use the map below // example if they are in node_modules, use the map below.
const pathToFileMap: PathToFileMap = { const pathToFileMap: PathToFileMap = {
'css/bulma.min.css': 'node_modules/bulma/css/bulma.min.css', 'css/bulma.min.css': 'node_modules/bulma/css/bulma.min.css',
'css/bulma-prefers-dark.min.css': 'node_modules/bulma-prefers-dark/css/bulma-prefers-dark.min.css', 'css/bulma-prefers-dark.min.css': 'node_modules/bulma-prefers-dark/css/bulma-prefers-dark.min.css',
'css/fontawesome/css/all.min.css': 'node_modules/@fortawesome/fontawesome-free/css/all.min.css',
}; };
async function findLocalFile(path: string): Promise<string> { async function findLocalFile(path: string): Promise<string> {
if (path in pathToFileMap) return pathToFileMap[path]; if (path in pathToFileMap) return pathToFileMap[path];
// For now a bit of a hack to load FontAwesome fonts.
if (path.indexOf('css/fontawesome/webfonts/fa-') === 0) return `node_modules/@fortawesome/fontawesome-free/${path.substr(16)}`;
let localPath = normalize(path); let localPath = normalize(path);
if (localPath.indexOf('..') >= 0) throw new ErrorNotFound(`Cannot resolve path: ${path}`); if (localPath.indexOf('..') >= 0) throw new ErrorNotFound(`Cannot resolve path: ${path}`);

View File

@@ -1,8 +1,8 @@
import { SubPath, Route } from '../../utils/routeUtils'; import { SubPath, Route, respondWithFileContent } from '../../utils/routeUtils';
import { AppContext } from '../../utils/types'; import { AppContext } from '../../utils/types';
import { contextSessionId } from '../../utils/requestUtils'; import { contextSessionId } from '../../utils/requestUtils';
import { ErrorMethodNotAllowed } from '../../utils/errors'; import { ErrorMethodNotAllowed, ErrorNotFound } from '../../utils/errors';
import { requestPagination } from '../../models/utils/pagination'; import { File } from '../../db';
const route: Route = { const route: Route = {
@@ -10,7 +10,14 @@ const route: Route = {
const sessionId = contextSessionId(ctx); const sessionId = contextSessionId(ctx);
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
return ctx.controllers.indexFiles().getIndex(sessionId, path.id, requestPagination(ctx.query)); if (!path.link) {
return ctx.controllers.indexFiles().getIndex(sessionId, path.id, ctx.query);
} else if (path.link === 'content') {
const file: File = await ctx.controllers.apiFile().getFileContent(sessionId, path.id);
return respondWithFileContent(ctx.response, file);
}
throw new ErrorNotFound();
} }
throw new ErrorMethodNotAllowed(); throw new ErrorMethodNotAllowed();

View File

@@ -1,4 +1,4 @@
import { ItemAddressingType } from '../db'; import { File, ItemAddressingType } from '../db';
import { ErrorBadRequest } from './errors'; import { ErrorBadRequest } from './errors';
import { AppContext } from './types'; import { AppContext } from './types';
@@ -145,7 +145,7 @@ export function parseSubPath(p: string): SubPath {
return output; return output;
} }
export function routeResponseFormat(match: MatchedRoute, rawPath:string): RouteResponseFormat { export function routeResponseFormat(match: MatchedRoute, rawPath: string): RouteResponseFormat {
if (match && match.route.responseFormat) return match.route.responseFormat; if (match && match.route.responseFormat) return match.route.responseFormat;
let path = rawPath; let path = rawPath;
@@ -190,3 +190,10 @@ export function findMatchingRoute(path: string, routes: Routes): MatchedRoute {
throw new Error('Unreachable'); throw new Error('Unreachable');
} }
export function respondWithFileContent(koaResponse: any, file: File): Response {
koaResponse.body = file.content;
koaResponse.set('Content-Type', file.mime_type);
koaResponse.set('Content-Length', file.size.toString());
return new Response(ResponseType.KoaResponse, koaResponse);
}

View File

@@ -1,4 +1,4 @@
/* eslint-disable import/prefer-default-export */ import dayjs = require('dayjs');
export function msleep(ms: number) { export function msleep(ms: number) {
return new Promise((resolve: Function) => { return new Promise((resolve: Function) => {
@@ -7,3 +7,7 @@ export function msleep(ms: number) {
}, ms); }, ms);
}); });
} }
export function formatDateTime(ms: number): string {
return dayjs(ms).format('D MMM YY HH:mm:ss');
}

View File

@@ -0,0 +1,13 @@
/* eslint-disable import/prefer-default-export */
import { URL } from 'url';
export function setQueryParameters(url: string, query: any): string {
const u = new URL(url);
for (const k of Object.keys(query)) {
u.searchParams.set(k, query[k]);
}
return u.toString();
}

View File

@@ -2,6 +2,9 @@
<div class="container"> <div class="container">
<div class="notification is-danger"> <div class="notification is-danger">
{{error.message}} {{error.message}}
{{#stack}}
<pre>{{.}}</pre>
{{/stack}}
</div> </div>
<p><a href="{{{global.baseUrl}}}/login">Back to the login page</a></p> <p><a href="{{{global.baseUrl}}}/login">Back to the login page</a></p>
</div> </div>

View File

@@ -1,5 +1,22 @@
<div> <table class="table is-fullwidth is-hoverable">
{{#paginatedFiles.items}} <thead>
<div>{{name}}</div> <tr>
{{/paginatedFiles.items}} <th class="stretch">Name</th>
</div> <th class="nowrap">Mime</th>
<th class="nowrap">Timestamp</th>
</tr>
</thead>
<tbody>
{{#paginatedFiles.items}}
<tr>
<td class="stretch item-{{type}}">
<a href="{{url}}"><span class="icon"><i class="{{icon}}"></i></span>{{name}}</a>
</td>
<td class="nowrap">{{mime}}</td>
<td class="nowrap">{{timestamp}}</td>
</tr>
{{/paginatedFiles.items}}
</tbody>
</table>
{{>pagination}}

View File

@@ -5,6 +5,7 @@
<link rel="stylesheet" href="{{{baseUrl}}}/css/bulma.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="{{{baseUrl}}}/css/bulma.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="{{{baseUrl}}}/css/bulma-prefers-dark.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="{{{baseUrl}}}/css/bulma-prefers-dark.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="{{{baseUrl}}}/css/main.css" crossorigin="anonymous"> <link rel="stylesheet" href="{{{baseUrl}}}/css/main.css" crossorigin="anonymous">
<link rel="stylesheet" href="{{{baseUrl}}}/css/fontawesome/css/all.min.css" crossorigin="anonymous">
<script src="{{{baseUrl}}}/js/main.js"></script> <script src="{{{baseUrl}}}/js/main.js"></script>
{{#cssFiles}} {{#cssFiles}}
<link rel="stylesheet" href="{{{.}}}" crossorigin="anonymous"> <link rel="stylesheet" href="{{{.}}}" crossorigin="anonymous">

View File

@@ -0,0 +1,18 @@
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<a class="pagination-previous">Previous</a>
<a class="pagination-next">Next page</a>
<ul class="pagination-list">
{{#paginationLinks}}
{{#isEllipsis}}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{{/isEllipsis}}
{{^isEllipsis}}
<li>
<a class="pagination-link {{#isCurrent}}is-current{{/isCurrent}}" aria-label="Goto page {{page}}" href="{{url}}">{{page}}</a>
</li>
{{/isEllipsis}}
{{/paginationLinks}}
</ul>
</nav>