Skip to content

feat(wopi): report the file's SHA-256 in CheckFileInfo - #6055

Open
timar wants to merge 1 commit into
mainfrom
feat/checkfileinfo-sha256
Open

timar wants to merge 1 commit into
mainfrom
feat/checkfileinfo-sha256

Conversation

@timar

@timar timar commented Sep 8, 2026

Copy link
Copy Markdown
Member

After an upload that gets no answer, the editor has to work out whether what is in storage is what it just sent. Without a hash to compare it falls back on the byte count, which cannot tell our own document from another editor's of the same length: the wrong answer either loses the other writer's work or puts a needless conflict in front of the user.

Report the file's SHA-256 in the CheckFileInfo SHA256 field so there is something better to compare. The field is only present when a hash is on record; CheckFileInfo is on the document-open path and must not read a whole file to compute one.

For it to be on record when it matters, hash the body of PutFile as it streams past and store the result, the way the DAV endpoint stores the checksums a sync client sends. Wrapping the stream costs no extra read, since the bytes pass through us in any case. The scanner clears the checksum whenever a file changes, so what we hand out is never stale: it either describes the current contents or is absent, and setting it after putContent() keeps it that way.

Files written by other means, or before this change, simply carry no SHA-256 and the editor falls back as it does today.

Checklist

  • Code is properly formatted
  • Sign-off message is added to all commits
  • Documentation (manuals or wiki) has been updated or is not required

@github-actions

Copy link
Copy Markdown
Contributor

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@timar

timar commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

ping @elzody @juliusknorr
Are you considering to take this feature? The COOL side has been merged already.

@timar
timar force-pushed the feat/checkfileinfo-sha256 branch from ae02f09 to 35bd166 Compare September 23, 2026 10:13
@elzody

elzody commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

@timar Will take a closer look at this soon.

@juliusknorr juliusknorr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally a good idea, some comments inline

Comment thread lib/Controller/WopiController.php Outdated
// whether what is in storage is what it uploaded. This costs no
// extra read: the bytes are going through us anyway.
$sha256 = null;
$content = HashWrapper::wrap($content, 'sha256', function (string $hash) use (&$sha256): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OC\Files\Stream\HashWrapper is private server API, so we can't rely on it staying stable across the server versions we support. Hashing on write also feels like a server job. nextcloud/server#56057 asks for exactly that, so every write path (DAV without a client checksum, other apps) would get a checksum.

Not a blocker I'd say but just for context.

Comment thread lib/Controller/WopiController.php Outdated
return new JSONResponse(['message' => 'File locked'], Http::STATUS_LOCKED);
}

$this->storeSha256($file, $sha256);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could store our hash on top of someone else's content. putContent() has already released the lock at this point. If another write lands between that and storeSha256(), the cache ends up with their content and our hash. That's exactly the case this is meant to catch, and the editor would then wrongly treat it as its own. The window is small, but I think it's worth closing. I'm unsure if we can perform a ourselves before the write without interfering with the write lock itself.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lack ideas how to mitigate that, the only way I see is that server stores the hash directly.

* Checksums are stored as space-separated TYPE:HEX pairs, so a file may
* carry an MD5 or SHA1 from a sync client and no SHA-256 at all.
*/
private function getStoredSha256(File $file): ?string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify this a bit? Iirc getChecksum() always returns a string, so the try/catch and the is_string check shouldn't be needed. Have not tested but something like this?

private function getStoredSha256(File $file): ?string {
	if (!preg_match('/(?:^| )SHA256:([0-9a-f]{64})(?: |$)/i', $file->getChecksum(), $matches)) {
		return null;
	}
	return base64_encode(hex2bin($matches[1]));
}

Comment thread lib/Controller/WopiController.php Outdated
return new JSONResponse(['message' => 'File locked'], Http::STATUS_LOCKED);
}

$this->storeSha256($file, $sha256);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a small integration test in tests/features/wopi.feature? Something like "PutFile, then CheckFileInfo returns the matching SHA256", and "SHA256 is absent after the file was changed via DAV".

@juliusknorr juliusknorr added the enhancement New feature or request label Sep 30, 2026
After an upload that gets no answer, the editor has to work out whether
what is in storage is what it just sent. Without a hash to compare it
falls back on the byte count, which cannot tell our own document from
another editor's of the same length: the wrong answer either loses the
other writer's work or puts a needless conflict in front of the user.

Report the file's SHA-256 in the CheckFileInfo SHA256 field so there is
something better to compare. The field is only present when a hash is on
record, and never for a version: CheckFileInfo is on the document-open
path and must not read a whole file to compute one, and the hash on
record describes the current contents, not an older version's.

For it to be on record when it matters, hash the body of PutFile as it
streams past and store the result in the cache field that holds the
checksums a sync client sends over DAV. A stream filter of our own does
the hashing, so that we do not lean on the server's private HashWrapper.
Wrapping the stream costs no extra read, since the bytes pass through us
in any case.

The hash is recorded from a NodeWrittenEvent listener registered for the
duration of the write. The server dispatches that event while it still
holds the lock it took for the write and after the cache has been
updated, which is also where the DAV endpoint persists its checksum. A
competing writer cannot take the lock until we are done, so the hash
always describes the bytes it was taken from. Recording it after
putContent() returned would have left a window in which another writer's
content ends up carrying our hash, the very confusion the hash is meant
to prevent.

The scanner clears the checksum whenever it sees the file change, and
DAV clears it when a client sends none, so what we hand out is never
stale: it either describes the current contents or is absent. Files
written by other means, or before this change, simply carry no SHA-256
and the editor falls back as it does today.

Signed-off-by: Andras Timar <andras.timar@collabora.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feedback-requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants