| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- // @ts-check
- 'use strict'
- const _ = require('lodash')
- const assert = require('check-types').assert
- const OError = require('@overleaf/o-error')
- const FileData = require('./file_data')
- const HashFileData = require('./file_data/hash_file_data')
- const StringFileData = require('./file_data/string_file_data')
- /**
- * @import Blob from "./blob"
- * @import { BlobStore, ReadonlyBlobStore, RawFileData, RawFile } from "./types"
- * @import { StringFileRawData, CommentRawData } from "./types"
- * @import CommentList from "./file_data/comment_list"
- * @import TextOperation from "./operation/text_operation"
- * @import TrackedChangeList from "./file_data/tracked_change_list"
- *
- * @typedef {{filterTrackedDeletes?: boolean}} FileGetContentOptions
- */
- class NotEditableError extends OError {
- constructor() {
- super('File is not editable')
- }
- }
- /**
- * A file in a {@link Snapshot}. A file has both data and metadata. There
- * are several classes of data that represent the various types of file
- * data that are supported, namely text and binary, and also the various
- * states that a file's data can be in, namely:
- *
- * 1. Hash only: all we know is the file's hash; this is how we encode file
- * content in long term storage.
- * 2. Lazily loaded: the hash of the file, its length, and its type are known,
- * but its content is not loaded. Operations are cached for application
- * later.
- * 3. Eagerly loaded: the content of a text file is fully loaded into memory
- * as a string.
- * 4. Hollow: only the byte and/or UTF-8 length of the file are known; this is
- * used to allow for validation of operations when editing collaboratively
- * without having to keep file data in memory on the server.
- */
- class File {
- /**
- * Blob hash for an empty file.
- *
- * @type {String}
- */
- static EMPTY_FILE_HASH = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
- static NotEditableError = NotEditableError
- /**
- * @param {FileData} data
- * @param {Object} [metadata]
- */
- constructor(data, metadata) {
- assert.instance(data, FileData, 'File: bad data')
- this.data = data
- this.metadata = {}
- this.setMetadata(metadata || {})
- }
- /**
- * @param {RawFile} raw
- * @return {File|null}
- */
- static fromRaw(raw) {
- if (!raw) return null
- return new File(FileData.fromRaw(raw), raw.metadata)
- }
- /**
- * @param {string} hash
- * @param {string} [rangesHash]
- * @param {Object} [metadata]
- * @return {File}
- */
- static fromHash(hash, rangesHash, metadata) {
- return new File(new HashFileData(hash, rangesHash), metadata)
- }
- /**
- * @param {string} string
- * @param {Object} [metadata]
- * @return {File}
- */
- static fromString(string, metadata) {
- return new File(new StringFileData(string), metadata)
- }
- /**
- * @param {number} byteLength
- * @param {number?} stringLength
- * @param {Object} [metadata]
- * @return {File}
- */
- static createHollow(byteLength, stringLength, metadata) {
- return new File(FileData.createHollow(byteLength, stringLength), metadata)
- }
- /**
- * @param {Blob} blob
- * @param {Blob} [rangesBlob]
- * @param {Object} [metadata]
- * @return {File}
- */
- static createLazyFromBlobs(blob, rangesBlob, metadata) {
- return new File(FileData.createLazyFromBlobs(blob, rangesBlob), metadata)
- }
- /**
- * @returns {RawFile}
- */
- toRaw() {
- /** @type RawFile */
- const rawFileData = this.data.toRaw()
- storeRawMetadata(this.metadata, rawFileData)
- return rawFileData
- }
- /**
- * Hexadecimal SHA-1 hash of the file's content, if known.
- *
- * @return {string | null | undefined}
- */
- getHash() {
- return this.data.getHash()
- }
- /**
- * Hexadecimal SHA-1 hash of the ranges content (comments + tracked changes),
- * if known.
- *
- * @return {string | null | undefined}
- */
- getRangesHash() {
- return this.data.getRangesHash()
- }
- /**
- * The content of the file, if it is known and if this file has UTF-8 encoded
- * content.
- *
- * @param {FileGetContentOptions} [opts]
- * @return {string | null | undefined}
- */
- getContent(opts = {}) {
- return this.data.getContent(opts)
- }
- /**
- * Whether this file has string content and is small enough to be edited using
- * {@link TextOperation}s.
- *
- * @return {boolean | null | undefined} null if it is not currently known
- */
- isEditable() {
- return this.data.isEditable()
- }
- /**
- * The length of the file's content in bytes, if known.
- *
- * @return {number | null | undefined}
- */
- getByteLength() {
- return this.data.getByteLength()
- }
- /**
- * The length of the file's content in characters, if known.
- *
- * @return {number | null | undefined}
- */
- getStringLength() {
- return this.data.getStringLength()
- }
- /**
- * Return the metadata object for this file.
- *
- * @return {Object}
- */
- getMetadata() {
- return this.metadata
- }
- /**
- * Set the metadata object for this file.
- *
- * @param {Object} metadata
- */
- setMetadata(metadata) {
- assert.object(metadata, 'File: bad metadata')
- this.metadata = metadata
- }
- /**
- * Edit this file, if possible.
- *
- * @param {TextOperation} textOperation
- */
- edit(textOperation) {
- if (!this.data.isEditable()) throw new File.NotEditableError()
- this.data.edit(textOperation)
- }
- /**
- * Get the comments for this file.
- *
- * @return {CommentList}
- */
- getComments() {
- return this.data.getComments()
- }
- /**
- * Get the tracked changes for this file.
- * @return {TrackedChangeList}
- */
- getTrackedChanges() {
- return this.data.getTrackedChanges()
- }
- /**
- * Clone a file.
- *
- * @return {File} a new object of the same type
- */
- clone() {
- return /** @type {File} */ (File.fromRaw(this.toRaw()))
- }
- /**
- * Convert this file's data to the given kind. This may require us to load file
- * size or content from the given blob store, so this is an asynchronous
- * operation.
- *
- * @param {string} kind
- * @param {ReadonlyBlobStore} blobStore
- * @return {Promise.<File>} for this
- */
- async load(kind, blobStore) {
- const data = await this.data.load(kind, blobStore)
- this.data = data
- return this
- }
- /**
- * Store the file's content in the blob store and return a raw file with
- * the corresponding hash. As a side effect, make this object consistent with
- * the hash.
- *
- * @param {BlobStore} blobStore
- * @return {Promise<RawFile>} a raw HashFile
- */
- async store(blobStore) {
- /** @type RawFile */
- const raw = await this.data.store(blobStore)
- storeRawMetadata(this.metadata, raw)
- return raw
- }
- }
- /**
- * @param {Object} metadata
- * @param {RawFile} raw
- */
- function storeRawMetadata(metadata, raw) {
- if (!_.isEmpty(metadata)) {
- raw.metadata = _.cloneDeep(metadata)
- }
- }
- module.exports = File
|