You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
3 Commits
v3.3.13
...
server_fil
Author | SHA1 | Date | |
---|---|---|---|
|
8801e82cab | ||
|
c4757d6c60 | ||
|
af6c79b844 |
@@ -1481,6 +1481,9 @@ packages/server/src/models/ChangeModel.test.js.map
|
||||
packages/server/src/models/FileModel.d.ts
|
||||
packages/server/src/models/FileModel.js
|
||||
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.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1470,6 +1470,9 @@ packages/server/src/models/ChangeModel.test.js.map
|
||||
packages/server/src/models/FileModel.d.ts
|
||||
packages/server/src/models/FileModel.js
|
||||
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.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
|
25
packages/server/package-lock.json
generated
25
packages/server/package-lock.json
generated
@@ -429,6 +429,11 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -1552,6 +1557,14 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"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": {
|
||||
@@ -2439,6 +2452,11 @@
|
||||
"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": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -7160,10 +7178,9 @@
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.1.0",
|
||||
|
@@ -12,10 +12,12 @@
|
||||
"watch": "tsc --watch --project tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"@joplin/lib": "^1.0.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bulma": "^0.9.1",
|
||||
"bulma-prefers-dark": "^0.1.0-beta.0",
|
||||
"dayjs": "^1.9.8",
|
||||
"formidable": "^1.2.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-entities": "^1.3.1",
|
||||
|
0
packages/server/public/css/index/files.css
Normal file
0
packages/server/public/css/index/files.css
Normal file
@@ -25,3 +25,11 @@ input.form-control {
|
||||
.main {
|
||||
padding: 0 3rem;
|
||||
}
|
||||
|
||||
table.table .nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.table .stretch {
|
||||
width: 100%;
|
||||
}
|
@@ -71,11 +71,11 @@ app.use(async (ctx: Koa.Context) => {
|
||||
}
|
||||
} catch (error) {
|
||||
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 {
|
||||
appLogger().error(error);
|
||||
}
|
||||
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
@@ -87,6 +87,7 @@ app.use(async (ctx: Koa.Context) => {
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: env === 'dev' ? error.stack : '',
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.renderView(view);
|
||||
@@ -111,8 +112,8 @@ async function main() {
|
||||
const globalLogger = new Logger();
|
||||
// globalLogger.addTarget(TargetType.File, { path: `${config().logDir}/app.txt` });
|
||||
globalLogger.addTarget(TargetType.Console, {
|
||||
format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s',
|
||||
formatInfo: '%(date_time)s: %(prefix)s: %(message)s',
|
||||
format: '%(date_time)s: [%(level)s] %(prefix)s: %(message)s',
|
||||
formatInfo: '%(date_time)s: %(prefix)s: %(message)s',
|
||||
});
|
||||
Logger.initializeGlobalLogger(globalLogger);
|
||||
|
||||
|
@@ -3,20 +3,20 @@ import configBase from './config-base';
|
||||
|
||||
const config: Config = {
|
||||
...configBase,
|
||||
// database: {
|
||||
// name: 'dev',
|
||||
// client: 'sqlite3',
|
||||
// asyncStackTraces: true,
|
||||
// },
|
||||
database: {
|
||||
client: 'pg',
|
||||
name: 'joplin',
|
||||
user: 'joplin',
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
password: 'joplin',
|
||||
name: 'dev',
|
||||
client: 'sqlite3',
|
||||
asyncStackTraces: true,
|
||||
},
|
||||
// database: {
|
||||
// client: 'pg',
|
||||
// name: 'joplin',
|
||||
// user: 'joplin',
|
||||
// host: 'localhost',
|
||||
// port: 5432,
|
||||
// password: 'joplin',
|
||||
// asyncStackTraces: true,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@@ -1,44 +1,85 @@
|
||||
import BaseController from '../BaseController';
|
||||
import { View } from '../../services/MustacheService';
|
||||
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 { 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 {
|
||||
|
||||
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 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 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 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);
|
||||
view.content.paginatedFiles = paginatedFiles;
|
||||
view.content.paginatedFiles = { ...paginatedFiles, items: files };
|
||||
view.content.paginationLinks = paginationLinks;
|
||||
view.cssFiles = ['index/files'];
|
||||
view.partials.push('pagination');
|
||||
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;
|
||||
// }
|
||||
|
||||
}
|
||||
|
@@ -332,6 +332,13 @@ export default class FileModel extends BaseModel {
|
||||
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> {
|
||||
const parent = await this.load(id);
|
||||
await this.checkCanReadPermissions(parent);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { expectThrow } from '../../utils/testUtils';
|
||||
import { defaultPagination, Pagination, requestPagination } from './pagination';
|
||||
import { defaultPagination, Pagination, createPaginationLinks, requestPagination } from './pagination';
|
||||
|
||||
describe('pagination', function() {
|
||||
|
||||
@@ -69,4 +69,56 @@ describe('pagination', function() {
|
||||
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 },
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -26,17 +26,17 @@ export interface PaginatedResults {
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
const pageMaxSize = 1000;
|
||||
const defaultOrderField = 'updated_time';
|
||||
const defaultOrderDir = PaginationOrderDir.DESC;
|
||||
export const pageMaxSize = 1000;
|
||||
const defaultOrderField_ = 'updated_time';
|
||||
const defaultOrderDir_ = PaginationOrderDir.DESC;
|
||||
|
||||
export function defaultPagination(): Pagination {
|
||||
return {
|
||||
limit: pageMaxSize,
|
||||
order: [
|
||||
{
|
||||
by: defaultOrderField,
|
||||
dir: defaultOrderDir,
|
||||
by: defaultOrderField_,
|
||||
dir: defaultOrderDir_,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
@@ -47,7 +47,10 @@ function dbOffset(pagination: Pagination): number {
|
||||
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 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.page <= 0) throw new ErrorBadRequest(`Invalid page number: ${p.page}`);
|
||||
|
||||
@@ -104,11 +107,74 @@ export function requestChangePagination(query: any): ChangePagination {
|
||||
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> {
|
||||
pagination = processCursor(pagination);
|
||||
|
||||
const orderSql: any[] = pagination.order.map(o => {
|
||||
return {
|
||||
column: o.by,
|
||||
order: o.dir,
|
||||
};
|
||||
});
|
||||
|
||||
const items = await query
|
||||
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||
.orderBy(orderSql)
|
||||
.offset(dbOffset(pagination))
|
||||
.limit(pagination.limit);
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ErrorNotFound, ErrorMethodNotAllowed, ErrorBadRequest } from '../../utils/errors';
|
||||
import { File } from '../../db';
|
||||
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 * as fs from 'fs-extra';
|
||||
import { requestChangePagination, requestPagination } from '../../models/utils/pagination';
|
||||
@@ -31,12 +31,8 @@ const route: Route = {
|
||||
|
||||
if (path.link === 'content') {
|
||||
if (ctx.method === 'GET') {
|
||||
const koaResponse = ctx.response;
|
||||
const file: File = await fileController.getFileContent(headerSessionId(ctx.headers), path.id);
|
||||
koaResponse.body = file.content;
|
||||
koaResponse.set('Content-Type', file.mime_type);
|
||||
koaResponse.set('Content-Length', file.size.toString());
|
||||
return new Response(ResponseType.KoaResponse, koaResponse);
|
||||
return respondWithFileContent(ctx.response, file);
|
||||
}
|
||||
|
||||
if (ctx.method === 'PUT') {
|
||||
|
@@ -13,14 +13,17 @@ interface PathToFileMap {
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
'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/fontawesome/css/all.min.css': 'node_modules/@fortawesome/fontawesome-free/css/all.min.css',
|
||||
};
|
||||
|
||||
async function findLocalFile(path: string): Promise<string> {
|
||||
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);
|
||||
if (localPath.indexOf('..') >= 0) throw new ErrorNotFound(`Cannot resolve path: ${path}`);
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { SubPath, Route } from '../../utils/routeUtils';
|
||||
import { SubPath, Route, respondWithFileContent } from '../../utils/routeUtils';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { contextSessionId } from '../../utils/requestUtils';
|
||||
import { ErrorMethodNotAllowed } from '../../utils/errors';
|
||||
import { requestPagination } from '../../models/utils/pagination';
|
||||
import { ErrorMethodNotAllowed, ErrorNotFound } from '../../utils/errors';
|
||||
import { File } from '../../db';
|
||||
|
||||
const route: Route = {
|
||||
|
||||
@@ -10,7 +10,14 @@ const route: Route = {
|
||||
const sessionId = contextSessionId(ctx);
|
||||
|
||||
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();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ItemAddressingType } from '../db';
|
||||
import { File, ItemAddressingType } from '../db';
|
||||
import { ErrorBadRequest } from './errors';
|
||||
import { AppContext } from './types';
|
||||
|
||||
@@ -145,7 +145,7 @@ export function parseSubPath(p: string): SubPath {
|
||||
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;
|
||||
|
||||
let path = rawPath;
|
||||
@@ -190,3 +190,10 @@ export function findMatchingRoute(path: string, routes: Routes): MatchedRoute {
|
||||
|
||||
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);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import dayjs = require('dayjs');
|
||||
|
||||
export function msleep(ms: number) {
|
||||
return new Promise((resolve: Function) => {
|
||||
@@ -7,3 +7,7 @@ export function msleep(ms: number) {
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDateTime(ms: number): string {
|
||||
return dayjs(ms).format('D MMM YY HH:mm:ss');
|
||||
}
|
||||
|
13
packages/server/src/utils/urlUtils.ts
Normal file
13
packages/server/src/utils/urlUtils.ts
Normal 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();
|
||||
}
|
@@ -2,6 +2,9 @@
|
||||
<div class="container">
|
||||
<div class="notification is-danger">
|
||||
{{error.message}}
|
||||
{{#stack}}
|
||||
<pre>{{.}}</pre>
|
||||
{{/stack}}
|
||||
</div>
|
||||
<p><a href="{{{global.baseUrl}}}/login">Back to the login page</a></p>
|
||||
</div>
|
||||
|
@@ -1,5 +1,22 @@
|
||||
<div>
|
||||
{{#paginatedFiles.items}}
|
||||
<div>{{name}}</div>
|
||||
{{/paginatedFiles.items}}
|
||||
</div>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="stretch">Name</th>
|
||||
<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}}
|
@@ -5,6 +5,7 @@
|
||||
<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/main.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{{baseUrl}}}/css/fontawesome/css/all.min.css" crossorigin="anonymous">
|
||||
<script src="{{{baseUrl}}}/js/main.js"></script>
|
||||
{{#cssFiles}}
|
||||
<link rel="stylesheet" href="{{{.}}}" crossorigin="anonymous">
|
||||
|
18
packages/server/src/views/partials/pagination.mustache
Normal file
18
packages/server/src/views/partials/pagination.mustache
Normal 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">…</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>
|
Reference in New Issue
Block a user