diff --git a/app/server/lib/DocStorage.ts b/app/server/lib/DocStorage.ts index 8ec7f65435..aaa4608eae 100644 --- a/app/server/lib/DocStorage.ts +++ b/app/server/lib/DocStorage.ts @@ -786,11 +786,7 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { storageId?: string, ): Promise { return this.execTransaction(async (db) => { - const isNewFile = await this._addBasicFileRecord(db, fileIdent); - if (isNewFile) { - await this._updateFileRecord(db, fileIdent, fileData, storageId); - } - return isNewFile; + return await this._addFileRecord(db, fileIdent, fileData, storageId); }); } @@ -813,9 +809,11 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { storageId?: string, ): Promise { return this.execTransaction(async (db) => { - const isNewFile = await this._addBasicFileRecord(db, fileIdent); - await this._updateFileRecord(db, fileIdent, fileData, storageId); - return isNewFile; + const wasAdded = await this._addFileRecord(db, fileIdent, fileData, storageId); + if (!wasAdded) { + await this._updateFileRecord(db, fileIdent, fileData, storageId); + } + return wasAdded; }); } @@ -823,7 +821,8 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { * Reads and returns the data for the given attachment. * @param {string} fileIdent - The unique identifier of a file, as used by attachFileIfNew. * @param {boolean} includeData - Load file contents from the database, in addition to metadata - * @returns {Promise[FileInfo | null]} - File information, or null if no record exists for that file identifier. + * @returns {Promise[FileInfo | null]} - File information, or null if no record exists for that + * file identifier. */ public getFileInfo(fileIdent: string, includeData: boolean = true): Promise { const columns = includeData ? 'ident, storageId, data' : 'ident, storageId'; @@ -938,9 +937,10 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { } /** - * Unmarshals and decodes data received from db.allMarshal() method (which we added to node-sqlite3). - * The data is a dictionary mapping column ids (including 'id') to arrays of values. This should - * be used for Grist data, which is encoded. For non-Grist data, use `marshal.loads()`. + * Unmarshals and decodes data received from db.allMarshal() method (which we added to + * node-sqlite3). The data is a dictionary mapping column ids (including 'id') to arrays of + * values. This should be used for Grist data, which is encoded. For non-Grist data, use + * `marshal.loads()`. * * Note that we do NOT use this when loading data from a document, since the whole point of * db.allMarshal() is to pass data directly to Python data engine without parsing in Node. @@ -1446,8 +1446,9 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { } /** - * Delete attachments from _gristsys_Files that have no matching metadata row in _grist_Attachments. - * This leaves any attachment files in any remote attachment stores, which will be cleaned up separately. + * Delete attachments from _gristsys_Files that have no matching metadata row in + * _grist_Attachments. This leaves any attachment files in any remote attachment stores, which + * will be cleaned up separately. */ public async removeUnusedAttachments() { const result = await this._getDB().run(` @@ -1861,18 +1862,22 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { return null; } - private async _addBasicFileRecord(db: SQLiteDB, fileIdent: string): Promise { - try { - // Try to insert a new record with the given ident. It'll fail UNIQUE constraint if exists. - await db.run('INSERT INTO _gristsys_Files (ident, data, storageId) VALUES (?)', fileIdent); - } catch(err) { - // If UNIQUE constraint failed, this ident must already exist. - if (/^(SQLITE_CONSTRAINT: )?UNIQUE constraint failed/.test(err.message)) { - return false; - } else { - throw err; - } + // This should be executed inside a transaction. + /** + * @returns {Promise} - True if the file was added, false if a record already exists. + * @private + */ + private async _addFileRecord( + db: SQLiteDB, fileIdent: string, fileData?: Buffer, storageId?: string + ): Promise { + const result = await db.get("SELECT 1 AS fileExists from _gristsys_Files WHERE ident = ?", fileIdent); + if (result?.fileExists) { + return false; } + await db.run( + 'INSERT INTO main._gristsys_Files (ident, data, storageId) VALUES (?, ?, ?)', + fileIdent, fileData, storageId + ); return true; } @@ -1881,6 +1886,7 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage { ): Promise { await db.run('UPDATE _gristsys_Files SET data=?, storageId=? WHERE ident=?', fileData, storageId, fileIdent); } + } interface RebuildResult { @@ -1905,7 +1911,8 @@ export interface IndexInfo extends IndexColumns { } /** - * Creates an index that allows fast SQL JOIN between _grist_Attachments.fileIdent and _gristsys_Files.ident. + * Creates an index that allows fast SQL JOIN between _grist_Attachments.fileIdent and + * _gristsys_Files.ident. */ export async function createAttachmentsIndex(db: ISQLiteDB) { await db.exec(`CREATE INDEX _grist_Attachments_fileIdent ON _grist_Attachments(fileIdent)`);