Skip to content

Commit

Permalink
feat: add a downloads page
Browse files Browse the repository at this point in the history
This should hopefully reduce confusion for users.
  • Loading branch information
aalemayhu committed Nov 12, 2023
1 parent ba273af commit 6beb1fe
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 50 deletions.
17 changes: 10 additions & 7 deletions src/controllers/DownloadController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import fs from 'fs';

import { static as serve, NextFunction, Request, Response } from 'express';
import { Request, Response } from 'express';

import path from 'path';
import { sendError } from '../lib/error/sendError';
import DownloadService from '../services/DownloadService';
import StorageHandler from '../lib/storage/StorageHandler';
import path from 'path';
import { DownloadPage } from '../pages/DownloadPage';
import DownloadService from '../services/DownloadService';
import { canAccess } from '../lib/misc/canAccess';

class DownloadController {
constructor(private service: DownloadService) {}
Expand Down Expand Up @@ -41,9 +42,10 @@ class DownloadController {

getDownloadPage(req: Request, res: Response) {
const { id } = req.params;
const workspace = path.join(process.env.WORKSPACE_BASE!, id);
const workspaceBase = process.env.WORKSPACE_BASE!;
const workspace = path.join(workspaceBase, id);

if (!fs.existsSync(workspace)) {
if (!fs.existsSync(workspace) || !canAccess(workspace, workspaceBase)) {
return res.status(404).end();
}

Expand All @@ -66,10 +68,11 @@ class DownloadController {

getLocalFile(req: Request, res: Response) {
const { id, filename } = req.params;
const workspace = path.join(process.env.WORKSPACE_BASE!, id);
const workspaceBase = process.env.WORKSPACE_BASE!;
const workspace = path.join(workspaceBase, id);
const filePath = path.join(workspace, filename);

if (!fs.existsSync(filePath)) {
if (!canAccess(filePath, workspace) || !fs.existsSync(filePath)) {
return res.status(404).end();
}
return res.sendFile(filePath);
Expand Down
29 changes: 1 addition & 28 deletions src/controllers/UploadController.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,11 @@
import fs from 'fs';
import path from 'path';

import express, { Response } from 'express';
import express from 'express';

import { getOwner } from '../lib/User/getOwner';
import { ZipHandler } from '../lib/anki/zip';
import { TEMPLATE_DIR } from '../lib/constants';
import { sendError } from '../lib/error/sendError';
import { getLimitMessage } from '../lib/misc/getLimitMessage';
import Package from '../lib/parser/Package';
import StorageHandler from '../lib/storage/StorageHandler';
import NotionService from '../services/NotionService';
import { toText } from '../services/NotionService/BlockHandler/helpers/deckNameToText';
import UploadService from '../services/UploadService';
import { getRandomUUID } from '../shared/helpers/getRandomUUID';

const setFilename = (res: Response, filename: string) => {
try {
res.set('File-Name', toText(filename));
} catch (err) {
sendError(err);
}
};

function loadREADME(): string {
return fs.readFileSync(path.join(TEMPLATE_DIR, 'README.html')).toString();
}

export const sendBundle = async (packages: Package[], res: Response) => {
const filename = `Your decks-${getRandomUUID()}.zip`;
const payload = await ZipHandler.toZip(packages, loadREADME());
setFilename(res, filename);
res.status(200).send(payload);
};

class UploadController {
constructor(
Expand Down
83 changes: 83 additions & 0 deletions src/lib/misc/canAccess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { canAccess } from "./canAccess";


test("returns false on path traversal", () => {
// Arrange
const directoryPath = '/tmp/..';

// Act
const access = canAccess(directoryPath);

// Assert
expect(access).toBe(false);
})

test("returns true for workspace path", () => {
// Arrange
const directoryPath = '/tmp/download/03d993ad-7b85-44bc-a810-aa3098a1b483'

// Act
const access = canAccess(directoryPath, '/tmp');

// Assert
expect(access).toBe(true);
})

test("returns false for relative path", () => {
// Arrange
const directoryPath = '~/.config';

// Act
const access = canAccess(directoryPath);

// Assert
expect(access).toBe(false);
})

test("returns false if outside of basePath", () => {
// Arrange
const directoryPath = '/home/user/Downloads';

// Act
const access = canAccess(directoryPath, '/tmp/workspace');

// Assert
expect(access).toBe(false);
})

test("returns true for APKG in workspace", () => {
// Arrange
const directoryPath = '/tmp/download/03d993ad-7b85-44bc-a810-aa3098a1b483/x.apkg'

// Act
const access = canAccess(directoryPath, '/tmp');

// Assert
expect(access).toBe(true);
})


test("returns false for newlines ", () => {
// Arrange
const newLines = "This is a long string"
+ " that spans multiple lines."
+ "\nIt can contain newlines"
+ " and other characters without any issues.";

// Act
const access = canAccess(newLines);

// Assert
expect(access).toBe(false);
})

test("returns false for long filename", () => {
// Arrange
const longString = "A musical instrument is a device created or adapted to make musical sounds. In principle, any object that produces sound can be considered a musical instrument—it is through purpose that the object becomes a musical instrument. A person who plays a musical instrument is known as an instrumentalist. The history of musical instruments dates to the beginnings of human culture. Early musical instruments may have been used for rituals, such as a horn to signal success on the hunt, or a drum in a religious ceremony. Cultures eventually developed composition and performance of melodies for entertainment. Musical instruments evolved in step with changing applications and technologies."

// Act
const access = canAccess(longString);

// Assert
expect(access).toBe(false);
})
23 changes: 23 additions & 0 deletions src/lib/misc/canAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const MAX_FILE_NAME_LENGTH = 255;

export const canAccess = (thePath: string, basePath?: string) => {
console.log('canAccess', thePath, basePath);

if (thePath.includes('..')) {
return false;
}

if (thePath.includes('~')) {
return false;
}

if (basePath) {
return thePath.startsWith(basePath);
}

if (thePath.length >= MAX_FILE_NAME_LENGTH) {
return false;
}

return /^[\w\-. ]+$/.test(thePath);
};
5 changes: 4 additions & 1 deletion src/lib/parser/WorkSpace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { getRandomUUID } from '../../shared/helpers/getRandomUUID';
class Workspace {
location: string;

id: string;

constructor(isNew: boolean, type: string) {
if (isNew && type === 'fs') {
this.location = path.join(process.env.WORKSPACE_BASE!, getRandomUUID());
this.id = getRandomUUID();
this.location = path.join(process.env.WORKSPACE_BASE!, this.id);
} else {
throw new Error(`unsupported ${type}`);
}
Expand Down
74 changes: 63 additions & 11 deletions src/pages/DownloadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,72 @@ interface DownloadPageProps {
}

export const DownloadPage = ({ id, files }: DownloadPageProps) => {
const styles = {
downloadContainer: {
margin: '0 auto',
maxWidth: '800px',
padding: '20px',
border: '1px solid #ccc',
},
downloadHeader: {
fontSize: '24px',
marginBottom: '20px',
},
downloadList: {
listStyle: 'none',
padding: '0',
margin: '0',
},
downloadItem: {
marginBottom: '10px',
display: 'flex',
justifyContent: 'space-between',
},
downloadItemName: {
display: 'block',
padding: '10px 20px',
backgroundColor: '#eee',
textDecoration: 'none',
color: '#000',
maxWidth: '80%',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
downloadItemLinkHover: {
backgroundColor: '#ddd',
},
downloadItemLink: {},
};
return ReactDOMServer.renderToStaticMarkup(
<html>
<header>
<head>
<title>Your download</title>
</header>
<body>
<h1>Your download is ready</h1>
{files.map((file) => (
<li key={file}>
<a download={`${path.basename(file)}`} href={`${id}/${file}`}>
{file}
</a>
</li>
))}
</head>
<body style={styles.downloadContainer}>
<header style={{ padding: '1rem' }}>
<h1 style={styles.downloadHeader}>Your downloads are ready</h1>
This is the list of Anki decks detected from your upload.
</header>
<main>
<ul style={styles.downloadList}>
{files.map((file) => (
<li key={file} style={styles.downloadItem}>
<span style={styles.downloadItemName}>{file}</span>
<a
style={styles.downloadItemLink}
download={`${path.basename(file)}`}
href={`${id}/${file}`}
>
Download
</a>
</li>
))}
</ul>
<p>
This folder will be automatically deleted. Return to
<a href="https://2anki.net">2anki.net</a>
</p>
</main>
</body>
</html>
);
Expand Down
17 changes: 14 additions & 3 deletions src/services/UploadService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import fs from 'fs';
import path from 'path';

import express from 'express';
import multer from 'multer';

import { sendBundle } from '../controllers/UploadController';
import { IUploadRepository } from '../data_layer/UploadRespository';
import { sendError } from '../lib/error/sendError';
import ErrorHandler, { NO_PACKAGE_ERROR } from '../lib/misc/ErrorHandler';
import { getUploadLimits } from '../lib/misc/getUploadLimits';
import Settings from '../lib/parser/Settings';
import Workspace from '../lib/parser/WorkSpace';
import StorageHandler from '../lib/storage/StorageHandler';
import { UploadedFile } from '../lib/storage/types';
import GeneratePackagesUseCase from '../usecases/uploads/GeneratePackagesUseCase';
Expand Down Expand Up @@ -71,8 +74,16 @@ class UploadService {
res.attachment(`/${first.name}`);
return res.status(200).send(payload);
} else if (packages.length > 1) {
await sendBundle(packages, res);
console.info('Sent bundle with %d packages', packages.length);
const workspace = new Workspace(true, 'fs');

for (const pkg of packages) {
const p = path.join(workspace.location, pkg.name);
fs.writeFileSync(p, pkg.apkg);
}

const url = `/download/${workspace.id}`;
res.status(300);
return res.redirect(url);
} else {
ErrorHandler(res, NO_PACKAGE_ERROR);
}
Expand Down

0 comments on commit 6beb1fe

Please sign in to comment.