"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileDataAccessor = void 0;
const fs_extra_1 = require("fs-extra");
const RepresentationMetadata_1 = require("../../http/representation/RepresentationMetadata");
const LogUtil_1 = require("../../logging/LogUtil");
const NotFoundHttpError_1 = require("../../util/errors/NotFoundHttpError");
const SystemError_1 = require("../../util/errors/SystemError");
const UnsupportedMediaTypeHttpError_1 = require("../../util/errors/UnsupportedMediaTypeHttpError");
const GuardedStream_1 = require("../../util/GuardedStream");
const HeaderUtil_1 = require("../../util/HeaderUtil");
const PathUtil_1 = require("../../util/PathUtil");
const QuadUtil_1 = require("../../util/QuadUtil");
const ResourceUtil_1 = require("../../util/ResourceUtil");
const TermUtil_1 = require("../../util/TermUtil");
const Vocabularies_1 = require("../../util/Vocabularies");
/**
 * DataAccessor that uses the file system to store documents as files and containers as folders.
 */
class FileDataAccessor {
    logger = (0, LogUtil_1.getLoggerFor)(this);
    resourceMapper;
    constructor(resourceMapper) {
        this.resourceMapper = resourceMapper;
    }
    /**
     * Only binary data can be directly stored as files so will error on non-binary data.
     */
    async canHandle(representation) {
        if (!representation.binary) {
            throw new UnsupportedMediaTypeHttpError_1.UnsupportedMediaTypeHttpError('Only binary data is supported.');
        }
    }
    /**
     * Will return data stream directly to the file corresponding to the resource.
     * Will throw NotFoundHttpError if the input is a container.
     */
    async getData(identifier) {
        const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
        const stats = await this.getStats(link.filePath);
        if (stats.isFile()) {
            return (0, GuardedStream_1.guardStream)((0, fs_extra_1.createReadStream)(link.filePath));
        }
        throw new NotFoundHttpError_1.NotFoundHttpError();
    }
    /**
     * Will return corresponding metadata by reading the metadata file (if it exists)
     * and adding file system specific metadata elements.
     */
    async getMetadata(identifier) {
        const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
        const stats = await this.getStats(link.filePath);
        if (!(0, PathUtil_1.isContainerIdentifier)(identifier) && stats.isFile()) {
            return this.getFileMetadata(link, stats);
        }
        if ((0, PathUtil_1.isContainerIdentifier)(identifier) && stats.isDirectory()) {
            return this.getDirectoryMetadata(link, stats);
        }
        throw new NotFoundHttpError_1.NotFoundHttpError();
    }
    async *getChildren(identifier) {
        const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
        yield* this.getChildMetadata(link);
    }
    /**
     * Writes the given data as a file (and potential metadata as additional file).
     * The metadata file will be written first and will be deleted if something goes wrong writing the actual data.
     */
    async writeDocument(identifier, data, metadata) {
        const link = await this.resourceMapper.mapUrlToFilePath(identifier, false, metadata.contentType);
        // Check if we already have a corresponding file with a different extension
        await this.verifyExistingExtension(link);
        const wroteMetadata = await this.writeMetadataFile(link, metadata);
        try {
            await this.writeDataFile(link.filePath, data);
        }
        catch (error) {
            // Delete the metadata if there was an error writing the file
            if (wroteMetadata) {
                const metaLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
                await (0, fs_extra_1.remove)(metaLink.filePath);
            }
            throw error;
        }
    }
    /**
     * Creates corresponding folder if necessary and writes metadata to metadata file if necessary.
     */
    async writeContainer(identifier, metadata) {
        const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
        await (0, fs_extra_1.ensureDir)(link.filePath);
        await this.writeMetadataFile(link, metadata);
    }
    async writeMetadata(identifier, metadata) {
        const metadataLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
        await this.writeMetadataFile(metadataLink, metadata);
    }
    /**
     * Removes the corresponding file/folder (and metadata file).
     */
    async deleteResource(identifier) {
        const metaLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
        await (0, fs_extra_1.remove)(metaLink.filePath);
        const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
        const stats = await this.getStats(link.filePath);
        if (!(0, PathUtil_1.isContainerIdentifier)(identifier) && stats.isFile()) {
            await (0, fs_extra_1.remove)(link.filePath);
        }
        else if ((0, PathUtil_1.isContainerIdentifier)(identifier) && stats.isDirectory()) {
            await (0, fs_extra_1.remove)(link.filePath);
        }
        else {
            throw new NotFoundHttpError_1.NotFoundHttpError();
        }
    }
    /**
     * Gets the Stats object corresponding to the given file path,
     * resolving symbolic links.
     * @param path - File path to get info from.
     *
     * @throws NotFoundHttpError
     * If the file/folder doesn't exist.
     */
    async getStats(path) {
        try {
            return await (0, fs_extra_1.stat)(path);
        }
        catch (error) {
            if ((0, SystemError_1.isSystemError)(error) && error.code === 'ENOENT') {
                throw new NotFoundHttpError_1.NotFoundHttpError('', { cause: error });
            }
            throw error;
        }
    }
    /**
     * Reads and generates all metadata relevant for the given file,
     * ingesting it into a RepresentationMetadata object.
     *
     * @param link - Path related metadata.
     * @param stats - Stats object of the corresponding file.
     */
    async getFileMetadata(link, stats) {
        const metadata = await this.getBaseMetadata(link, stats, false);
        // If the resource is using an unsupported contentType, the original contentType was written to the metadata file.
        // As a result, we should only set the contentType derived from the file path,
        // when no previous metadata entry for contentType is present.
        if (typeof metadata.contentType === 'undefined') {
            metadata.set(Vocabularies_1.CONTENT_TYPE_TERM, link.contentType);
        }
        return metadata;
    }
    /**
     * Reads and generates all metadata relevant for the given directory,
     * ingesting it into a RepresentationMetadata object.
     *
     * @param link - Path related metadata.
     * @param stats - Stats object of the corresponding directory.
     */
    async getDirectoryMetadata(link, stats) {
        return await this.getBaseMetadata(link, stats, true);
    }
    /**
     * Writes the metadata of the resource to a meta file.
     * @param link - Path related metadata of the resource.
     * @param metadata - Metadata to write.
     *
     * @returns True if data was written to a file.
     */
    async writeMetadataFile(link, metadata) {
        // These are stored by file system conventions
        metadata.remove(Vocabularies_1.RDF.terms.type, Vocabularies_1.LDP.terms.Resource);
        metadata.remove(Vocabularies_1.RDF.terms.type, Vocabularies_1.LDP.terms.Container);
        metadata.remove(Vocabularies_1.RDF.terms.type, Vocabularies_1.LDP.terms.BasicContainer);
        metadata.removeAll(Vocabularies_1.DC.terms.modified);
        // When writing metadata for a document, only remove the content-type when dealing with a supported media type.
        // A media type is supported if the FileIdentifierMapper can correctly store it.
        // This allows restoring the appropriate content-type on data read (see getFileMetadata).
        if ((0, PathUtil_1.isContainerPath)(link.filePath) || typeof link.contentType !== 'undefined') {
            metadata.removeAll(Vocabularies_1.CONTENT_TYPE_TERM);
        }
        const quads = metadata.quads();
        const metadataLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, true);
        let wroteMetadata;
        // Write metadata to file if there are quads remaining
        if (quads.length > 0) {
            // Determine required content-type based on mapper
            const serializedMetadata = (0, QuadUtil_1.serializeQuads)(quads, metadataLink.contentType);
            await this.writeDataFile(metadataLink.filePath, serializedMetadata);
            wroteMetadata = true;
            // Delete (potentially) existing metadata file if no metadata needs to be stored
        }
        else {
            await (0, fs_extra_1.remove)(metadataLink.filePath);
            wroteMetadata = false;
        }
        return wroteMetadata;
    }
    /**
     * Generates metadata relevant for any resources stored by this accessor.
     * @param link - Path related metadata.
     * @param stats - Stats objects of the corresponding directory.
     * @param isContainer - If the path points to a container (directory) or not.
     */
    async getBaseMetadata(link, stats, isContainer) {
        const metadata = new RepresentationMetadata_1.RepresentationMetadata(link.identifier)
            .addQuads(await this.getRawMetadata(link.identifier));
        (0, ResourceUtil_1.addResourceMetadata)(metadata, isContainer);
        this.addPosixMetadata(metadata, stats);
        return metadata;
    }
    /**
     * Reads the metadata from the corresponding metadata file.
     * Returns an empty array if there is no metadata file.
     *
     * @param identifier - Identifier of the resource (not the metadata!).
     */
    async getRawMetadata(identifier) {
        try {
            const metadataLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
            // Check if the metadata file exists first
            await (0, fs_extra_1.lstat)(metadataLink.filePath);
            const readMetadataStream = (0, GuardedStream_1.guardStream)((0, fs_extra_1.createReadStream)(metadataLink.filePath));
            return await (0, QuadUtil_1.parseQuads)(readMetadataStream, { format: metadataLink.contentType, baseIRI: identifier.path });
        }
        catch (error) {
            // Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
            if (!(0, SystemError_1.isSystemError)(error) || error.code !== 'ENOENT') {
                throw error;
            }
            return [];
        }
    }
    /**
     * Generate metadata for all children in a container.
     *
     * @param link - Path related metadata.
     */
    async *getChildMetadata(link) {
        const dir = await (0, fs_extra_1.opendir)(link.filePath);
        // For every child in the container we want to generate specific metadata
        for await (const entry of dir) {
            // Obtain details of the entry, resolving any symbolic links
            const childPath = (0, PathUtil_1.joinFilePath)(link.filePath, entry.name);
            let childStats;
            try {
                childStats = await this.getStats(childPath);
            }
            catch {
                // Skip this entry if details could not be retrieved (e.g., bad symbolic link)
                continue;
            }
            // Ignore non-file/directory entries in the folder
            if (!childStats.isFile() && !childStats.isDirectory()) {
                continue;
            }
            // Generate the URI corresponding to the child resource
            const childLink = await this.resourceMapper.mapFilePathToUrl(childPath, childStats.isDirectory());
            // Hide metadata files
            if (childLink.isMetadata) {
                continue;
            }
            // Generate metadata of this specific child as described in
            // https://solidproject.org/TR/2021/protocol-20211217#contained-resource-metadata
            const metadata = new RepresentationMetadata_1.RepresentationMetadata(childLink.identifier);
            (0, ResourceUtil_1.addResourceMetadata)(metadata, childStats.isDirectory());
            this.addPosixMetadata(metadata, childStats);
            // Containers will not have a content-type
            const { contentType, identifier } = childLink;
            if (contentType) {
                // Make sure we don't generate invalid URIs
                try {
                    const { value } = (0, HeaderUtil_1.parseContentType)(contentType);
                    metadata.add(Vocabularies_1.RDF.terms.type, (0, TermUtil_1.toNamedTerm)(`${Vocabularies_1.IANA.namespace}${value}#Resource`));
                }
                catch {
                    this.logger.warn(`Detected an invalid content-type "${contentType}" for ${identifier.path}`);
                }
            }
            yield metadata;
        }
    }
    /**
     * Helper function to add file system related metadata.
     * @param metadata - metadata object to add to
     * @param stats - Stats of the file/directory corresponding to the resource.
     */
    addPosixMetadata(metadata, stats) {
        (0, ResourceUtil_1.updateModifiedDate)(metadata, stats.mtime);
        metadata.add(Vocabularies_1.POSIX.terms.mtime, (0, TermUtil_1.toLiteral)(Math.floor(stats.mtime.getTime() / 1000), Vocabularies_1.XSD.terms.integer), Vocabularies_1.SOLID_META.terms.ResponseMetadata);
        if (!stats.isDirectory()) {
            metadata.add(Vocabularies_1.POSIX.terms.size, (0, TermUtil_1.toLiteral)(stats.size, Vocabularies_1.XSD.terms.integer), Vocabularies_1.SOLID_META.terms.ResponseMetadata);
        }
    }
    /**
     * Verifies if there already is a file corresponding to the given resource.
     * If yes, that file is removed if it does not match the path given in the input ResourceLink.
     * This can happen if the content-type differs from the one that was stored.
     *
     * @param link - ResourceLink corresponding to the new resource data.
     */
    async verifyExistingExtension(link) {
        // Delete the old file with the (now) wrong extension
        const oldLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, false);
        if (oldLink.filePath !== link.filePath) {
            await (0, fs_extra_1.remove)(oldLink.filePath);
        }
    }
    /**
     * Helper function without extra validation checking to create a data file.
     * @param path - The filepath of the file to be created.
     * @param data - The data to be put in the file.
     */
    async writeDataFile(path, data) {
        return new Promise((resolve, reject) => {
            const writeStream = (0, fs_extra_1.createWriteStream)(path);
            data.pipe(writeStream);
            data.on('error', (error) => {
                reject(error);
                writeStream.end();
            });
            writeStream.on('error', reject);
            writeStream.on('finish', resolve);
        });
    }
}
exports.FileDataAccessor = FileDataAccessor;
//# sourceMappingURL=FileDataAccessor.js.map