@solid/community-server
Version:
Community Solid Server: an open and modular implementation of the Solid specifications
180 lines • 8.69 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseResourcesGenerator = void 0;
const node_fs_1 = require("node:fs");
const fs_extra_1 = require("fs-extra");
const n3_1 = require("n3");
const global_logger_factory_1 = require("global-logger-factory");
const BasicRepresentation_1 = require("../../http/representation/BasicRepresentation");
const RepresentationMetadata_1 = require("../../http/representation/RepresentationMetadata");
const ContentTypes_1 = require("../../util/ContentTypes");
const GuardedStream_1 = require("../../util/GuardedStream");
const PathUtil_1 = require("../../util/PathUtil");
const ResourceUtil_1 = require("../../util/ResourceUtil");
const StreamUtil_1 = require("../../util/StreamUtil");
// Comparator for the results of the `groupLinks` call
function comparator(left, right) {
return left.link.identifier.path.localeCompare(right.link.identifier.path);
}
/**
* Generates resources by making use of a template engine.
* The template folder structure will be kept.
* Folders will be interpreted as containers and files as documents.
* A FileIdentifierMapper will be used to generate identifiers that correspond to the relative structure.
*
* Metadata resources will be yielded separately from their subject resource.
*
* A relative `templateFolder` is resolved relative to cwd,
* unless it's preceded by `@css:`, e.g. `@css:foo/bar`.
*/
class BaseResourcesGenerator {
logger = (0, global_logger_factory_1.getLoggerFor)(this);
factory;
templateEngine;
templateExtension;
metadataStrategy;
store;
/**
* A mapper is needed to convert the template file paths to identifiers relative to the given base identifier.
*
* @param args - TemplatedResourcesGeneratorArgs
*/
constructor(args) {
this.factory = args.factory;
this.templateEngine = args.templateEngine;
this.templateExtension = args.templateExtension ?? '.hbs';
this.metadataStrategy = args.metadataStrategy;
this.store = args.store;
}
async *generate(templateFolder, location, options) {
templateFolder = (0, PathUtil_1.resolveAssetPath)(templateFolder);
// Ignore folders that don't exist
if (!await (0, fs_extra_1.pathExists)(templateFolder)) {
this.logger.warn(`Ignoring non-existing template folder ${templateFolder}`);
return;
}
const mapper = await this.factory.create(location.path, templateFolder);
const folderLink = await this.toTemplateLink(templateFolder, mapper);
yield* this.processFolder(folderLink, mapper, options);
}
/**
* Generates results for all entries in the given folder, including the folder itself.
*/
async *processFolder(folderLink, mapper, options) {
// Group resource links with their corresponding metadata links
const links = await this.groupLinks(folderLink.filePath, mapper);
// Remove root metadata if it exists
const metaLink = links[folderLink.identifier.path]?.meta;
delete links[folderLink.identifier.path];
yield* this.generateResource(folderLink, options, metaLink);
// Make sure the results are sorted
for (const { link, meta } of Object.values(links).sort(comparator)) {
if ((0, PathUtil_1.isContainerIdentifier)(link.identifier)) {
yield* this.processFolder(link, mapper, options);
}
else {
yield* this.generateResource(link, options, meta);
}
}
}
/**
* Creates a TemplateResourceLink for the given filePath,
* which connects a resource URL to its template file.
* The identifier will be based on the file path stripped from the template extension,
* but the filePath parameter will still point to the original file.
*/
async toTemplateLink(filePath, mapper) {
const stats = await node_fs_1.promises.lstat(filePath);
// Slice the template extension from the filepath for correct identifier generation
const isTemplate = filePath.endsWith(this.templateExtension);
const slicedPath = isTemplate ? filePath.slice(0, -this.templateExtension.length) : filePath;
const link = await mapper.mapFilePathToUrl(slicedPath, stats.isDirectory());
// We still need the original file path for disk reading though
return {
...link,
filePath,
isTemplate,
};
}
/**
* Generates TemplateResourceLinks for each entry in the given folder
* and combines the results so resources and their metadata are grouped together.
*/
async groupLinks(folderPath, mapper) {
const files = await node_fs_1.promises.readdir(folderPath);
const links = {};
for (const name of files) {
const link = await this.toTemplateLink((0, PathUtil_1.joinFilePath)(folderPath, name), mapper);
const { path } = link.identifier;
links[path] = Object.assign(links[path] || {}, link.isMetadata ? { meta: link } : { link });
}
return links;
}
/**
* Generates a Resource object for the given ResourceLink.
* In the case of documents the corresponding template will be used.
* If a ResourceLink of metadata is provided the corresponding metadata resource
* will be yielded as a separate resource.
*/
async *generateResource(link, options, metaLink) {
let data;
const metadata = new RepresentationMetadata_1.RepresentationMetadata(link.identifier);
// Read file if it is not a container
if (!(0, PathUtil_1.isContainerIdentifier)(link.identifier)) {
data = await this.processFile(link, options);
metadata.contentType = link.contentType;
}
// Add metadata from .meta file if there is one
if (metaLink) {
const rawMetadata = await this.generateMetadata(metaLink, options);
if (rawMetadata.contentType) {
// Prevent having 2 content types
metadata.contentType = undefined;
}
metadata.setMetadata(rawMetadata);
this.logger.debug(`Adding metadata for ${metaLink.identifier.path}`);
}
const shouldYield = !(0, PathUtil_1.isContainerIdentifier)(link.identifier) || !await this.store.hasResource(link.identifier);
if (shouldYield) {
this.logger.debug(`Generating resource ${link.identifier.path}`);
yield {
identifier: link.identifier,
representation: new BasicRepresentation_1.BasicRepresentation(data ?? [], metadata),
};
}
// Still need to yield metadata in case the actual resource is not being yielded.
// We also do this for containers as existing containers can't be edited in the same way.
if (metaLink && (!shouldYield || (0, PathUtil_1.isContainerIdentifier)(link.identifier))) {
const metaIdentifier = this.metadataStrategy.getAuxiliaryIdentifier(link.identifier);
(0, ResourceUtil_1.addResourceMetadata)(metadata, (0, PathUtil_1.isContainerIdentifier)(link.identifier));
this.logger.debug(`Generating resource ${metaIdentifier.path}`);
yield {
identifier: metaIdentifier,
representation: new BasicRepresentation_1.BasicRepresentation(metadata.quads(), metaIdentifier, ContentTypes_1.INTERNAL_QUADS),
};
}
}
/**
* Generates a RepresentationMetadata using the given template.
*/
async generateMetadata(metaLink, options) {
const metadata = new RepresentationMetadata_1.RepresentationMetadata(metaLink.identifier);
const data = await this.processFile(metaLink, options);
const parser = new n3_1.Parser({ format: metaLink.contentType, baseIRI: metaLink.identifier.path });
const quads = parser.parse(await (0, StreamUtil_1.readableToString)(data));
metadata.addQuads(quads);
return metadata;
}
/**
* Creates a read stream from the file and applies the template if necessary.
*/
async processFile(link, contents) {
if (link.isTemplate) {
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: link.filePath } });
return (0, StreamUtil_1.guardedStreamFrom)(rendered);
}
return (0, GuardedStream_1.guardStream)((0, node_fs_1.createReadStream)(link.filePath));
}
}
exports.BaseResourcesGenerator = BaseResourcesGenerator;
//# sourceMappingURL=BaseResourcesGenerator.js.map