Skip to content

Commit

Permalink
Refactor file extension handling (#57)
Browse files Browse the repository at this point in the history
* Add a fileName util to generate a file name from a File object

This factorize the 'choosing png or html' logic into one place.
Add unit tests as well.

* use the fileName util where possible

* PR feedback

* typo

* Bump version number

Co-authored-by: Basile Simon <basile@basilesimon.fr>
  • Loading branch information
Cgg and basilesimon authored Aug 20, 2021
1 parent eaff96f commit 6978004
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 48 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"email": "niko@niko.io"
}
],
"version": "0.8.0",
"version": "0.9.1",
"repository": {
"type": "git",
"url": "https://github.com/digitalevidencetoolkit/deptoolkit"
Expand Down
32 changes: 19 additions & 13 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,16 @@ export const writeOne = async (a: File.newFile): Promise<File.File> => {
}
// create the directory if it does not yet exist
await mkdir(dir, { recursive: true });
const name = makeHash(a.data);
const format = a.kind === 'one_file' ? 'html' : 'png';
const path = `${dir}/${name}.${format}`;
await writeToDisk(a.data, path);
return { kind: a.kind, hash: name };

const { kind, data } = a;

const hash = makeHash(data);
const result: File.File = { kind, hash };

const path = `${dir}/${File.fileName(result)}`;
await writeToDisk(data, path);

return result;
};

/**
Expand All @@ -63,8 +68,9 @@ export const newBundle = (b: Bundle.newBundle): Promise<Bundle.Bundle> => {
* Generates a string describing the specified Record `r`.
*/
export const generateAboutString = (r: Record.Record): string => {
const screenshot = r.bundle.find(e => e.kind === 'screenshot').hash + '.png';
const one_file = r.bundle.find(e => e.kind === 'one_file').hash + '.html';
const screenshot = File.fileName(r.bundle.find(e => e.kind === 'screenshot'));
const one_file = File.fileName(r.bundle.find(e => e.kind === 'one_file'));

return `THE DIGITAL EVIDENCE PRESERVATION TOOLKIT
============
Working copy export generated on ${Date.now()}
Expand Down Expand Up @@ -105,8 +111,8 @@ export const makeZip = (
// presently the ZIP file only includes the full screenshot and one-file
// HTML archive. isolating them like so isn't the most elegant.
// maybe replace with a function from `Bundle`?
const screenshot = b.find(e => e.kind === 'screenshot').hash + '.png';
const one_file = b.find(e => e.kind === 'one_file').hash + '.html';
const screenshotName = File.fileName(b.find(e => e.kind === 'screenshot'));
const one_fileName = File.fileName(b.find(e => e.kind === 'one_file'));
const sidecarTextFile = generateAboutString(r);

const stream = fs.createWriteStream(out);
Expand All @@ -121,18 +127,18 @@ export const makeZip = (
zip
.append(
fs
.createReadStream(`${bundleRootDirectory}/${screenshot}`)
.createReadStream(`${bundleRootDirectory}/${screenshotName}`)
.on('error', reject),
{
name: screenshot,
name: screenshotName,
}
)
.append(
fs
.createReadStream(`${bundleRootDirectory}/${one_file}`)
.createReadStream(`${bundleRootDirectory}/${one_fileName}`)
.on('error', reject),
{
name: one_file,
name: one_fileName,
}
)
.append(sidecarTextFile, { name: `about-this-export.txt` })
Expand Down
71 changes: 37 additions & 34 deletions src/store/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import * as fs from 'fs';
import * as path from 'path';
import Zip from 'node-stream-zip';

import type { Bundle } from '../types/Bundle';
import type { Record } from '../types/Record';
import * as Bundle from '../types/Bundle';
import * as Record from '../types/Record';
import * as File from '../types/File';

import * as Store from './index';
import { id } from '../types/Bundle';

describe('generateAboutString', () => {
it('should throw if the record is missing the screenshot or one_file', () => {
const bundles: Bundle[] = [
const bundles: Bundle.Bundle[] = [
[],
[{ hash: 'jeej', kind: 'one_file' }],
[{ hash: 'tuut', kind: 'screenshot' }],
Expand All @@ -35,16 +35,16 @@ describe('generateAboutString', () => {
it('should generate a string describing the given bundle', () => {
const title = 'Win big money in no time thanks to this one simple trick';
const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
const oneFile = 'this-is-the-file';
const screenshot = 'pretty-picture';
const oneFile: File.File = { hash: 'this-is-the-file', kind: 'one_file' };
const screenshot: File.File = {
hash: 'pretty-picture',
kind: 'screenshot',
};
const ogNow = Date.now;
Date.now = () => 42;

const result = Store.generateAboutString({
bundle: [
{ hash: oneFile, kind: 'one_file' },
{ hash: screenshot, kind: 'screenshot' },
],
bundle: [oneFile, screenshot],
annotations: {
description: '',
},
Expand All @@ -62,8 +62,8 @@ ${title}
${url}
Files included:
${screenshot}.png
${oneFile}.html`;
${File.fileName(screenshot)}
${File.fileName(oneFile)}`;

expect(result).toEqual(expected);

Expand All @@ -87,7 +87,7 @@ describe('makeZip', () => {
});

it('should reject if the record is missing the screenshot or one_file', () => {
const bundles: Bundle[] = [
const bundles: Bundle.Bundle[] = [
[],
[{ hash: 'jeej', kind: 'one_file' }],
[{ hash: 'tuut', kind: 'screenshot' }],
Expand Down Expand Up @@ -116,63 +116,66 @@ describe('makeZip', () => {
});

it('should produce a zip of the given record, in the correct location', async () => {
const oneFileHash = 'this-is-the-file';
const screenshotHash = 'pretty-picture';
const oneFile: File.File = { hash: 'this-is-the-file', kind: 'one_file' };
const screenshot: File.File = {
hash: 'pretty-picture',
kind: 'screenshot',
};
const oneFileName = File.fileName(oneFile);
const screenshotName = File.fileName(screenshot);

fs.writeFileSync(path.join(bundleRootDir, `${oneFileHash}.html`), 'jeej');
fs.writeFileSync(path.join(bundleRootDir, `${screenshotHash}.png`), 'tuut');
fs.writeFileSync(path.join(bundleRootDir, oneFileName), 'jeej');
fs.writeFileSync(path.join(bundleRootDir, screenshotName), 'tuut');

const record: Record = {
const record: Record.Record = {
data: {
title: 'Non Stop Nyan Cat',
url: 'http://www.nyan.cat/',
},
annotations: {
description: 'A cat farting an infinite rainbow',
},
bundle: [
{ kind: 'one_file', hash: oneFileHash },
{ kind: 'screenshot', hash: screenshotHash },
],
bundle: [oneFile, screenshot],
};

await Store.makeZip(record, bundleRootDir, outDir);

const zipPath = path.join(outDir, `${id(record.bundle)}.zip`);
const zipPath = path.join(outDir, `${Bundle.id(record.bundle)}.zip`);

// check for the zip's existence and contents
// TODO: for this test to be complete, we should also check the files
// contents themselves.
const z = new Zip.async({ file: zipPath });
const entries = await z.entries();
expect(entries).toEqual({
[`${oneFileHash}.html`]: expect.any(Object),
[`${screenshotHash}.png`]: expect.any(Object),
[oneFileName]: expect.any(Object),
[screenshotName]: expect.any(Object),
'about-this-export.txt': expect.any(Object),
});
z.close();
});

it('should return a rejected promise when a file is missing on disk', async () => {
const oneFileHash = 'this-is-the-file';
const screenshotHash = 'pretty-picture';
const oneFile: File.File = { hash: 'this-is-the-file', kind: 'one_file' };
const screenshot: File.File = {
hash: 'pretty-picture',
kind: 'screenshot',
};
const screenshotName = File.fileName(screenshot);

// simulate a missing file
// fs.writeFileSync(path.join(bundleRootDir, `${oneFileHash}.html`), 'jeej');
fs.writeFileSync(path.join(bundleRootDir, `${screenshotHash}.png`), 'tuut');
// fs.writeFileSync(path.join(bundleRootDir, oneFileName), 'jeej');
fs.writeFileSync(path.join(bundleRootDir, screenshotName), 'tuut');

const record: Record = {
const record: Record.Record = {
data: {
title: 'Non Stop Nyan Cat',
url: 'http://www.nyan.cat/',
},
annotations: {
description: 'A cat farting an infinite rainbow',
},
bundle: [
{ kind: 'one_file', hash: oneFileHash },
{ kind: 'screenshot', hash: screenshotHash },
],
bundle: [oneFile, screenshot],
};

await Store.makeZip(record, bundleRootDir, outDir)
Expand Down
26 changes: 26 additions & 0 deletions src/types/File.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as File from './File';

describe('id', () => {
it('should return the file hash', () => {
const hash = 'I should have been a pair of ragged claws';
expect(File.id({ kind: 'one_file', hash })).toBe(hash);
});
});

describe('fileName', () => {
it('should generate the file name by adding the correct extention to the hash', () => {
const cases: Array<File.File & { expected: string }> = [
{ hash: 'The gate is straight', kind: 'one_file', expected: 'html' },
{ hash: 'Deep and wide', kind: 'screenshot', expected: 'png' },
{
hash: 'Break on through to the other side',
kind: 'screenshot_thumbnail',
expected: 'png',
},
];

cases.forEach(c => {
expect(File.fileName(c)).toBe(`${c.hash}.${c.expected}`);
});
});
});
8 changes: 8 additions & 0 deletions src/types/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ export type newFile = { kind: Process; data: Buffer | string };
* @returns string
*/
export const id = (a: File) => a.hash;

/**
* Generates a complete filename from a given file.
* @returns A complete filename, with the correct extension according to the
* file's kind.
*/
export const fileName: (f: File) => string = ({ kind, hash }) =>
`${hash}.${kind === 'one_file' ? 'html' : 'png'}`;

0 comments on commit 6978004

Please sign in to comment.