Skip to content

Commit dc47155

Browse files
Merge pull request #9149 from nextcloud/fix/no-conflicts-on-sync
fix/no conflicts on sync
2 parents 4be77e2 + d331c8f commit dc47155

3 files changed

Lines changed: 21 additions & 47 deletions

File tree

lib/Service/ApiService.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ public function sync(Session $session, Document $document, int $version = 0, ?st
175175
// ensure file is still present and accessible
176176
$file = $this->fileService->getFileForSession($session, $shareToken);
177177
$result['readOnly'] = $this->fileService->isReadOnly($file, $shareToken);
178-
$this->documentService->assertNoOutsideConflict($document, $file);
179178
} catch (NotPermittedException|NotFoundException|InvalidPathException $e) {
180179
$this->logger->info($e->getMessage(), ['exception' => $e]);
181180
return new DataResponse([
@@ -186,16 +185,9 @@ public function sync(Session $session, Document $document, int $version = 0, ?st
186185
return new DataResponse([
187186
'message' => 'Document no longer exists'
188187
], Http::STATUS_NOT_FOUND);
189-
} catch (DocumentSaveConflictException) {
190-
try {
191-
/** @psalm-suppress PossiblyUndefinedVariable */
192-
$result['outsideChange'] = $file->getContent();
193-
} catch (LockedException) {
194-
// Ignore locked exception since it might happen due to an autosave action happening at the same time
195-
}
196188
}
197189

198-
return new DataResponse($result, isset($result['outsideChange']) ? Http::STATUS_CONFLICT : Http::STATUS_OK);
190+
return new DataResponse($result, Http::STATUS_OK);
199191
}
200192

201193
public function save(Session $session, Document $document, int $version, string $autosaveContent, string $documentState, bool $force = false, bool $manualSave = false, ?string $shareToken = null): DataResponse {

lib/Service/DocumentService.php

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,6 @@ public function getSteps(int $documentId, int $lastVersion): array {
307307

308308
/**
309309
* @throws DocumentSaveConflictException
310-
* @throws DoesNotExistException
311310
* @throws InvalidPathException
312311
* @throws NotFoundException
313312
*/
@@ -332,16 +331,7 @@ public function assertNoOutsideConflict(Document $document, File $file, bool $fo
332331
$fileChecksum = self::computeCheckSum($fileContent);
333332

334333
if ($storedChecksum !== $fileChecksum) {
335-
// $document was loaded at the start of the request.
336-
// A save request handled in the meantime is not reflected in it
337-
// and would be mistaken for an outside change.
338-
// Reload the document to compare against the latest saved state.
339-
$document = $this->documentMapper->find($documentId);
340-
if ($document->getChecksum() !== $fileChecksum) {
341-
throw new DocumentSaveConflictException('File changed in the meantime from outside');
342-
}
343-
// The save request already stored the latest version info.
344-
return;
334+
throw new DocumentSaveConflictException('File changed in the meantime from outside');
345335
}
346336

347337
$document->setLastSavedVersionTime($fileMtime);
@@ -374,6 +364,11 @@ public function autosave(Document $document, File $file, int $version, string $a
374364

375365
$this->assertNoOutsideConflict($document, $file, $force);
376366

367+
// Abort autosave if already saving.
368+
if ($this->cache->get('document-save-lock-' . $documentId) && $manualSave === false) {
369+
return $document;
370+
}
371+
377372
// Do not save if newer version already saved
378373
// Note that $version is the version of the steps the client has fetched.
379374
// It may have added steps on top of that - so if the versions match we still save.
@@ -412,7 +407,7 @@ public function autosave(Document $document, File $file, int $version, string $a
412407
return $document;
413408
}
414409

415-
$this->cache->set('document-save-lock-' . $documentId, true, 10);
410+
$this->cache->set('document-save-lock-' . $documentId, true, 60);
416411
try {
417412
$this->lockService->runInScope($file, function () use ($file, $autoSaveDocument, $documentState): void {
418413
$this->saveFromText = true;

tests/unit/Service/DocumentServiceTest.php

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ public function testNoConflictWhenVersionInfoMatches(): void {
8383
$document = $this->createDocument('etag1', 1000, 'content');
8484
$file = $this->mockFile('etag1', 1000, 'content');
8585

86-
$this->documentMapper->expects(self::never())->method('find');
8786
$this->documentMapper->expects(self::never())->method('update');
88-
8987
$this->documentService->assertNoOutsideConflict($document, $file);
9088
}
9189

@@ -103,33 +101,9 @@ public function testRefreshesVersionInfoWhenContentMatches(): void {
103101
self::assertSame(2000, $document->getLastSavedVersionTime());
104102
}
105103

106-
public function testNoConflictWhenOwnSaveFinishedInTheMeantime(): void {
107-
// Loaded at the start of the request - stale by now.
108-
$document = $this->createDocument('etag1', 1000, 'old content');
109-
// A save request updated the file in the meantime ...
110-
$file = $this->mockFile('etag2', 2000, 'new content');
111-
// ... and stored the new version info in the document.
112-
$freshDocument = $this->createDocument('etag2', 2000, 'new content');
113-
114-
$this->documentMapper->expects(self::once())
115-
->method('find')
116-
->with(123)
117-
->willReturn($freshDocument);
118-
$this->documentMapper->expects(self::never())->method('update');
119-
120-
$this->documentService->assertNoOutsideConflict($document, $file);
121-
}
122-
123104
public function testConflictWhenFileChangedFromOutside(): void {
124105
$document = $this->createDocument('etag1', 1000, 'old content');
125106
$file = $this->mockFile('etag2', 2000, 'outside content');
126-
// The latest saved state does not match the file either.
127-
$freshDocument = $this->createDocument('etag1', 1000, 'old content');
128-
129-
$this->documentMapper->expects(self::once())
130-
->method('find')
131-
->with(123)
132-
->willReturn($freshDocument);
133107

134108
$this->expectException(DocumentSaveConflictException::class);
135109
$this->documentService->assertNoOutsideConflict($document, $file);
@@ -147,4 +121,17 @@ public function testNoConflictWhileSaveLockIsHeld(): void {
147121

148122
$this->documentService->assertNoOutsideConflict($document, $file);
149123
}
124+
125+
public function testNoAutosavingWhileSaveIsUnderWay(): void {
126+
$document = $this->createDocument('etag1', 1000, 'new content');
127+
$file = $this->mockFile('etag1', 1000, 'old content');
128+
$this->cache->method('get')
129+
->with('document-save-lock-123')
130+
->willReturn(true);
131+
$this->documentMapper->expects(self::never())->method('update');
132+
133+
$result = $this->documentService->autosave($document, $file, 1234, 'new content', 'doc state');
134+
self::assertSame($result, $document);
135+
}
136+
150137
}

0 commit comments

Comments
 (0)