|
|
@ -15,7 +15,7 @@ import {IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
|
|
|
import {ChecksummedExternalStorage, DELETED_TOKEN, ExternalStorage, Unchanged} from 'app/server/lib/ExternalStorage';
|
|
|
|
import {ChecksummedExternalStorage, DELETED_TOKEN, ExternalStorage, Unchanged} from 'app/server/lib/ExternalStorage';
|
|
|
|
import {HostedMetadataManager} from 'app/server/lib/HostedMetadataManager';
|
|
|
|
import {HostedMetadataManager} from 'app/server/lib/HostedMetadataManager';
|
|
|
|
import {ICreate} from 'app/server/lib/ICreate';
|
|
|
|
import {ICreate} from 'app/server/lib/ICreate';
|
|
|
|
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
|
|
|
import {IDocStorageManager, SnapshotProgress} from 'app/server/lib/IDocStorageManager';
|
|
|
|
import {LogMethods} from "app/server/lib/LogMethods";
|
|
|
|
import {LogMethods} from "app/server/lib/LogMethods";
|
|
|
|
import {fromCallback} from 'app/server/lib/serverUtils';
|
|
|
|
import {fromCallback} from 'app/server/lib/serverUtils';
|
|
|
|
import * as fse from 'fs-extra';
|
|
|
|
import * as fse from 'fs-extra';
|
|
|
@ -94,6 +94,9 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
// Time at which document was last changed.
|
|
|
|
// Time at which document was last changed.
|
|
|
|
private _timestamps = new Map<string, string>();
|
|
|
|
private _timestamps = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Statistics related to snapshot generation.
|
|
|
|
|
|
|
|
private _snapshotProgress = new Map<string, SnapshotProgress>();
|
|
|
|
|
|
|
|
|
|
|
|
// Access external storage.
|
|
|
|
// Access external storage.
|
|
|
|
private _ext: ChecksummedExternalStorage;
|
|
|
|
private _ext: ChecksummedExternalStorage;
|
|
|
|
private _extMeta: ChecksummedExternalStorage;
|
|
|
|
private _extMeta: ChecksummedExternalStorage;
|
|
|
@ -223,6 +226,25 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
return path.basename(altDocName, '.grist');
|
|
|
|
return path.basename(altDocName, '.grist');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Read some statistics related to generating snapshots.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
public getSnapshotProgress(docName: string): SnapshotProgress {
|
|
|
|
|
|
|
|
let snapshotProgress = this._snapshotProgress.get(docName);
|
|
|
|
|
|
|
|
if (!snapshotProgress) {
|
|
|
|
|
|
|
|
snapshotProgress = {
|
|
|
|
|
|
|
|
pushes: 0,
|
|
|
|
|
|
|
|
skippedPushes: 0,
|
|
|
|
|
|
|
|
errors: 0,
|
|
|
|
|
|
|
|
changes: 0,
|
|
|
|
|
|
|
|
windowsStarted: 0,
|
|
|
|
|
|
|
|
windowsDone: 0,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
this._snapshotProgress.set(docName, snapshotProgress);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return snapshotProgress;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Prepares a document for use locally. Here we sync the doc from S3 to the local filesystem.
|
|
|
|
* Prepares a document for use locally. Here we sync the doc from S3 to the local filesystem.
|
|
|
|
* Returns whether the document is new (needs to be created).
|
|
|
|
* Returns whether the document is new (needs to be created).
|
|
|
@ -476,7 +498,11 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
* This is called when a document may have been changed, via edits or migrations etc.
|
|
|
|
* This is called when a document may have been changed, via edits or migrations etc.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
public markAsChanged(docName: string, reason?: string): void {
|
|
|
|
public markAsChanged(docName: string, reason?: string): void {
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
const snapshotProgress = this.getSnapshotProgress(docName);
|
|
|
|
|
|
|
|
snapshotProgress.lastChangeAt = now.getTime();
|
|
|
|
|
|
|
|
snapshotProgress.changes++;
|
|
|
|
|
|
|
|
const timestamp = now.toISOString();
|
|
|
|
this._timestamps.set(docName, timestamp);
|
|
|
|
this._timestamps.set(docName, timestamp);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (parseUrlId(docName).snapshotId) { return; }
|
|
|
|
if (parseUrlId(docName).snapshotId) { return; }
|
|
|
@ -486,6 +512,10 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this._disableS3) { return; }
|
|
|
|
if (this._disableS3) { return; }
|
|
|
|
if (this._closed) { throw new Error("HostedStorageManager.markAsChanged called after closing"); }
|
|
|
|
if (this._closed) { throw new Error("HostedStorageManager.markAsChanged called after closing"); }
|
|
|
|
|
|
|
|
if (!this._uploads.hasPendingOperation(docName)) {
|
|
|
|
|
|
|
|
snapshotProgress.lastWindowStartedAt = now.getTime();
|
|
|
|
|
|
|
|
snapshotProgress.windowsStarted++;
|
|
|
|
|
|
|
|
}
|
|
|
|
this._uploads.addOperation(docName);
|
|
|
|
this._uploads.addOperation(docName);
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
if (reason === 'edit') {
|
|
|
|
if (reason === 'edit') {
|
|
|
@ -729,6 +759,7 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
private async _pushToS3(docId: string): Promise<void> {
|
|
|
|
private async _pushToS3(docId: string): Promise<void> {
|
|
|
|
let tmpPath: string|null = null;
|
|
|
|
let tmpPath: string|null = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const snapshotProgress = this.getSnapshotProgress(docId);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
if (this._prepareFiles.has(docId)) {
|
|
|
|
if (this._prepareFiles.has(docId)) {
|
|
|
|
throw new Error('too soon to consider pushing');
|
|
|
|
throw new Error('too soon to consider pushing');
|
|
|
@ -748,14 +779,18 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
await this._inventory.uploadAndAdd(docId, async () => {
|
|
|
|
await this._inventory.uploadAndAdd(docId, async () => {
|
|
|
|
const prevSnapshotId = this._latestVersions.get(docId) || null;
|
|
|
|
const prevSnapshotId = this._latestVersions.get(docId) || null;
|
|
|
|
const newSnapshotId = await this._ext.upload(docId, tmpPath as string, metadata);
|
|
|
|
const newSnapshotId = await this._ext.upload(docId, tmpPath as string, metadata);
|
|
|
|
|
|
|
|
snapshotProgress.lastWindowDoneAt = Date.now();
|
|
|
|
|
|
|
|
snapshotProgress.windowsDone++;
|
|
|
|
if (newSnapshotId === Unchanged) {
|
|
|
|
if (newSnapshotId === Unchanged) {
|
|
|
|
// Nothing uploaded because nothing changed
|
|
|
|
// Nothing uploaded because nothing changed
|
|
|
|
|
|
|
|
snapshotProgress.skippedPushes++;
|
|
|
|
return { prevSnapshotId };
|
|
|
|
return { prevSnapshotId };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!newSnapshotId) {
|
|
|
|
if (!newSnapshotId) {
|
|
|
|
// This is unexpected.
|
|
|
|
// This is unexpected.
|
|
|
|
throw new Error('No snapshotId allocated after upload');
|
|
|
|
throw new Error('No snapshotId allocated after upload');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
snapshotProgress.pushes++;
|
|
|
|
const snapshot = {
|
|
|
|
const snapshot = {
|
|
|
|
lastModified: t,
|
|
|
|
lastModified: t,
|
|
|
|
snapshotId: newSnapshotId,
|
|
|
|
snapshotId: newSnapshotId,
|
|
|
@ -767,6 +802,10 @@ export class HostedStorageManager implements IDocStorageManager {
|
|
|
|
if (changeMade) {
|
|
|
|
if (changeMade) {
|
|
|
|
await this._onInventoryChange(docId);
|
|
|
|
await this._onInventoryChange(docId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
snapshotProgress.errors++;
|
|
|
|
|
|
|
|
// Snapshot window completion time deliberately not set.
|
|
|
|
|
|
|
|
throw e;
|
|
|
|
} finally {
|
|
|
|
} finally {
|
|
|
|
// Clean up backup.
|
|
|
|
// Clean up backup.
|
|
|
|
// NOTE: fse.remove succeeds also when the file does not exist.
|
|
|
|
// NOTE: fse.remove succeeds also when the file does not exist.
|
|
|
|