Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Save a checksum for documents and use it to detect conflicts
Signed-off-by: Benjamin Frueh <[email protected]>
  • Loading branch information
benjaminfrueh authored and backportbot[bot] committed Oct 6, 2025
commit 0cd6a1f6f7366497ed0c329a4c0998ef5451cf16
1 change: 1 addition & 0 deletions composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
'OCA\\Text\\Migration\\Version030701Date20230207131313' => $baseDir . '/../lib/Migration/Version030701Date20230207131313.php',
'OCA\\Text\\Migration\\Version030901Date20231114150437' => $baseDir . '/../lib/Migration/Version030901Date20231114150437.php',
'OCA\\Text\\Migration\\Version040100Date20240611165300' => $baseDir . '/../lib/Migration/Version040100Date20240611165300.php',
'OCA\\Text\\Migration\\Version070000Date20250925110024' => $baseDir . '/../lib/Migration/Version070000Date20250925110024.php',
'OCA\\Text\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
'OCA\\Text\\Service\\ApiService' => $baseDir . '/../lib/Service/ApiService.php',
'OCA\\Text\\Service\\AttachmentService' => $baseDir . '/../lib/Service/AttachmentService.php',
Expand Down
1 change: 1 addition & 0 deletions composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ComposerStaticInitText
'OCA\\Text\\Migration\\Version030701Date20230207131313' => __DIR__ . '/..' . '/../lib/Migration/Version030701Date20230207131313.php',
'OCA\\Text\\Migration\\Version030901Date20231114150437' => __DIR__ . '/..' . '/../lib/Migration/Version030901Date20231114150437.php',
'OCA\\Text\\Migration\\Version040100Date20240611165300' => __DIR__ . '/..' . '/../lib/Migration/Version040100Date20240611165300.php',
'OCA\\Text\\Migration\\Version070000Date20250925110024' => __DIR__ . '/..' . '/../lib/Migration/Version070000Date20250925110024.php',
'OCA\\Text\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
'OCA\\Text\\Service\\ApiService' => __DIR__ . '/..' . '/../lib/Service/ApiService.php',
'OCA\\Text\\Service\\AttachmentService' => __DIR__ . '/..' . '/../lib/Service/AttachmentService.php',
Expand Down
7 changes: 6 additions & 1 deletion lib/Db/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
* @method setLastSavedVersionEtag(string $etag): void
* @method getBaseVersionEtag(): string
* @method setBaseVersionEtag(string $etag): void
* @method getChecksum(): ?string
* @method setChecksum(?string $checksum): void
*/
class Document extends Entity implements \JsonSerializable {
public $id = null;
Expand All @@ -32,13 +34,15 @@ class Document extends Entity implements \JsonSerializable {
protected int $lastSavedVersionTime = 0;
protected string $lastSavedVersionEtag = '';
protected string $baseVersionEtag = '';
protected ?string $checksum = null;

public function __construct() {
$this->addType('id', 'integer');
$this->addType('currentVersion', 'integer');
$this->addType('lastSavedVersion', 'integer');
$this->addType('lastSavedVersionTime', 'integer');
$this->addType('initialVersion', 'integer');
$this->addType('checksum', 'string');
}

public function jsonSerialize(): array {
Expand All @@ -47,7 +51,8 @@ public function jsonSerialize(): array {
'lastSavedVersion' => $this->lastSavedVersion,
'lastSavedVersionTime' => $this->lastSavedVersionTime,
'baseVersionEtag' => $this->baseVersionEtag,
'initialVersion' => $this->initialVersion
'initialVersion' => $this->initialVersion,
'checksum' => $this->checksum
];
}
}
32 changes: 32 additions & 0 deletions lib/Migration/Version070000Date20250925110024.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace OCA\Text\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

#[AddColumn(table: 'text_documents', name: 'checksum', type: ColumnType::STRING, description: 'CRC32 checksum of document content')]
class Version070000Date20250925110024 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

$table = $schema->getTable('text_documents');
if (!$table->hasColumn('checksum')) {
$table->addColumn('checksum', Types::STRING, [
'notnull' => false,
'length' => 8,
]);
return $schema;
}

return null;
}
}
41 changes: 33 additions & 8 deletions lib/Service/DocumentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public function createDocument(File $file): Document {
$document->setLastSavedVersionTime($file->getMTime());
$document->setLastSavedVersionEtag($file->getEtag());
$document->setBaseVersionEtag(uniqid());
$document->setChecksum($this->computeCheckSum($file->getContent()));
try {
/** @var Document $document */
$document = $this->documentMapper->insert($document);
Expand Down Expand Up @@ -310,25 +311,48 @@ public function getSteps(int $documentId, int $lastVersion): array {
return $this->stepMapper->find($documentId, $lastVersion);
}



/**
* @throws DocumentSaveConflictException
* @throws InvalidPathException
* @throws NotFoundException
*/
public function assertNoOutsideConflict(Document $document, File $file, bool $force = false, ?string $shareToken = null): void {
$documentId = $document->getId();
$savedEtag = $file->getEtag();
$lastMTime = $document->getLastSavedVersionTime();
$lastEtag = $document->getLastSavedVersionEtag();

if ($lastMTime <= 0 || $force || $this->isReadOnly($file, $shareToken) || $this->cache->get('document-save-lock-' . $documentId)) {
return;
}

$fileMtime = $file->getMtime();
$fileEtag = $file->getEtag();

if ($lastEtag === $fileEtag && $lastMTime === $fileMtime) {
return;
}

$storedChecksum = $document->getChecksum();
$fileContent = $file->getContent();
$fileChecksum = $this->computeChecksum($fileContent);

if ($lastMTime > 0
&& $force === false
&& !$this->isReadOnly($file, $shareToken)
&& $savedEtag !== $document->getLastSavedVersionEtag()
&& $lastMTime !== $file->getMtime()
&& !$this->cache->get('document-save-lock-' . $documentId)
) {
if ($storedChecksum !== $fileChecksum) {
throw new DocumentSaveConflictException('File changed in the meantime from outside');
}

$document->setLastSavedVersionTime($fileMtime);
$document->setLastSavedVersionEtag($fileEtag);
$this->documentMapper->update($document);
}

/**
* @param string $content
* @return string
*/
private function computeCheckSum(string $content): string {
return hash('crc32', $content);
}

/**
Expand Down Expand Up @@ -414,6 +438,7 @@ public function autosave(Document $document, ?File $file, int $version, ?string
$document->setLastSavedVersion($version);
$document->setLastSavedVersionTime($file->getMTime());
$document->setLastSavedVersionEtag($file->getEtag());
$document->setChecksum($this->computeCheckSum($autoSaveDocument));
$this->documentMapper->update($document);
} catch (LockedException $e) {
// Ignore lock since it might occur when multiple people save at the same time
Expand Down