Skip to content

Commit

Permalink
fix(package export): long filenames in content paths shortened
Browse files Browse the repository at this point in the history
  • Loading branch information
sr258 committed Jul 8, 2020
1 parent b5e5310 commit 433f067
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 19 deletions.
3 changes: 2 additions & 1 deletion src/H5PEditor.ts
Expand Up @@ -123,7 +123,8 @@ export default class H5PEditor {
);
this.packageExporter = new PackageExporter(
this.libraryStorage,
this.contentStorage
this.contentStorage,
config
);
this.semanticsLocalizer = new SemanticsLocalizer(translationCallback);
this.dependencyGetter = new DependencyGetter(libraryStorage);
Expand Down
115 changes: 98 additions & 17 deletions src/PackageExporter.ts
@@ -1,5 +1,6 @@
import { Writable, Readable } from 'stream';
import yazl from 'yazl';
import upath from 'upath';

import DependencyGetter from './DependencyGetter';
import H5pError from './helpers/H5pError';
Expand All @@ -12,8 +13,12 @@ import {
IUser,
Permission
} from './types';

import { ContentFileScanner } from './ContentFileScanner';
import Logger from './helpers/Logger';
import LibraryManager from './LibraryManager';
import generateFilename from './helpers/FilenameGenerator';
import { generalizedSanitizeFilename } from './implementation/utils';

const log = new Logger('PackageExporter');

/**
Expand All @@ -26,11 +31,15 @@ export default class PackageExporter {
*/
constructor(
private libraryStorage: ILibraryStorage,
private contentStorage: IContentStorage = null
private contentStorage: IContentStorage = null,
{ exportMaxContentPathLength }: { exportMaxContentPathLength: number }
) {
log.info(`initialize`);
this.maxContentPathLength = exportMaxContentPathLength;
}

private maxContentPathLength: number;

/**
* Creates a .h5p-package for the specified content file and pipes it to the stream.
* Throws H5pErrors if something goes wrong. The contents of the stream should be disregarded then.
Expand All @@ -49,20 +58,37 @@ export default class PackageExporter {
const outputZipFile = new yazl.ZipFile();
outputZipFile.outputStream.pipe(outputStream);

// add json files
const contentStream = await this.createContentFileStream(
// get content data
const parameters = await this.contentStorage.getParameters(
contentId,
user
);
outputZipFile.addReadStream(contentStream, 'content/content.json');
const { metadata, metadataStream } = await this.getMetadata(
contentId,
user
);

// check if filenames are too long and shorten them in the parameters
// if necessary; the substitutions that took place are returned and
// later used to change the paths inside the zip archive
const substitutions = await this.shortenFilenames(
parameters,
metadata,
this.maxContentPathLength
);

// add json files
const contentStream = await this.createContentFileStream(parameters);
outputZipFile.addReadStream(contentStream, 'content/content.json');
outputZipFile.addReadStream(metadataStream, 'h5p.json');

// add content file (= files in content directory)
await this.addContentFiles(contentId, user, outputZipFile);
await this.addContentFiles(
contentId,
user,
outputZipFile,
substitutions
);

// add library files
await this.addLibraryFiles(metadata, outputZipFile);
Expand All @@ -73,11 +99,18 @@ export default class PackageExporter {

/**
* Adds the files inside the content directory to the zip file. Does not include content.json!
* @param contentId the contentId of the content
* @param user the user who wants to export
* @param outputZipFile the file to write to
* @param pathSubstitutions list of unix (!) paths to files whose paths were
* changed in the parameters; this means the paths in the zip file must
* be changed accordingly
*/
private async addContentFiles(
contentId: ContentId,
user: IUser,
outputZipFile: yazl.ZipFile
outputZipFile: yazl.ZipFile,
pathSubstitutions: { [oldPath: string]: string }
): Promise<void> {
log.info(`adding content files to ${contentId}`);
const contentFiles = await this.contentStorage.listFiles(
Expand All @@ -92,7 +125,9 @@ export default class PackageExporter {
contentFile,
user
),
`content/${contentFile}`
`content/${
pathSubstitutions[upath.toUnix(contentFile)] ?? contentFile
}`
);
}
}
Expand Down Expand Up @@ -159,21 +194,14 @@ export default class PackageExporter {
/**
* Creates a readable stream for the content.json file
*/
private async createContentFileStream(
contentId: ContentId,
user: IUser
): Promise<Readable> {
private async createContentFileStream(parameters: any): Promise<Readable> {
let contentStream: Readable;
try {
const content = await this.contentStorage.getParameters(
contentId,
user
);
contentStream = new Readable();
contentStream._read = () => {
return;
};
contentStream.push(JSON.stringify(content));
contentStream.push(JSON.stringify(parameters));
contentStream.push(null);
} catch (error) {
throw new H5pError('download-content-unreadable-data');
Expand Down Expand Up @@ -204,4 +232,57 @@ export default class PackageExporter {

return { metadata, metadataStream };
}

/**
* Scans the parameters of the piece of content and looks for paths that are
* longed than the specified max length. If this happens the filenames are
* shortened in the parameters and the substitution is returned in the
* substitution list
* @param parameters the parameters to scan; IMPORTANT: The parameters are
* mutated by this method!!!
* @param metadata the metadata of the piece of content
* @param maxFilenameLength the maximum acceptable filename length
* @returns an object whose keys are old paths and values the new paths to
* be used instead; IMPORTANT: All paths are unix paths using slashes as
* directory separators!
*/
private async shortenFilenames(
parameters: any,
metadata: IContentMetadata,
maxFilenameLength: number
): Promise<{ [oldPath: string]: string }> {
const substitutions: { [oldPath: string]: string } = {};
const usedFilenames: { [filename: string]: boolean } = {};

const contentScanner = new ContentFileScanner(
new LibraryManager(this.libraryStorage)
);
const files = await contentScanner.scanForFiles(
parameters,
metadata.preloadedDependencies.find(
(dep) => dep.machineName === metadata.mainLibrary
)
);

for (const file of files) {
if (file.filePath.length >= maxFilenameLength) {
const newFilename = await generateFilename(
file.filePath,
(filenameToCheck) =>
generalizedSanitizeFilename(
filenameToCheck,
new RegExp(''),
maxFilenameLength - 9
),
async (fileToCheck) => usedFilenames[fileToCheck]
);
substitutions[file.filePath] = newFilename;
file.context.params.path = newFilename;
usedFilenames[newFilename] = true;
} else {
usedFilenames[file.filePath] = true;
}
}
return substitutions;
}
}
3 changes: 3 additions & 0 deletions src/implementation/H5PConfig.ts
Expand Up @@ -38,6 +38,7 @@ export default class H5PConfig implements IH5PConfig {
};
public editorLibraryUrl: string = '/editor';
public enableLrsContentTypes: boolean = true;
public exportMaxContentPathLength: number = 255;
public fetchingDisabled: 0 | 1 = 0;
public h5pVersion: string = '1.24.0';
public hubContentTypesEndpoint: string =
Expand Down Expand Up @@ -86,6 +87,7 @@ export default class H5PConfig implements IH5PConfig {
await this.loadSettingFromStorage('sendUsageStatistics');
await this.loadSettingFromStorage('siteType');
await this.loadSettingFromStorage('uuid');
await this.loadSettingFromStorage('exportMaxContentPathLength');
return this;
}

Expand All @@ -108,6 +110,7 @@ export default class H5PConfig implements IH5PConfig {
await this.saveSettingToStorage('sendUsageStatistics');
await this.saveSettingToStorage('siteType');
await this.saveSettingToStorage('uuid');
await this.saveSettingToStorage('exportMaxContentPathLength');
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Expand Up @@ -1333,6 +1333,11 @@ export interface IH5PConfig {
* User-configurable.
*/
enableLrsContentTypes: boolean;
/**
* The maximum character count of paths of content files allowed when
* exporting h5p packages. If files would be longer, paths are shortened.
*/
exportMaxContentPathLength: number;
/**
* Unclear. Taken over from PHP implementation and sent to the H5P Hub when registering the site.
* User-configurable.
Expand Down
3 changes: 2 additions & 1 deletion test/PackageExporter.test.ts
Expand Up @@ -40,7 +40,8 @@ export async function importAndExportPackage(

const packageExporter = new PackageExporter(
libraryStorage,
contentStorage
contentStorage,
{ exportMaxContentPathLength: 255 }
);
const contentId = (
await packageImporter.addPackageLibrariesAndContent(
Expand Down

0 comments on commit 433f067

Please sign in to comment.