Skip to content

fix(libsync): set read-only permissions after rename, not before#10213

Open
Lenart12 wants to merge 1 commit into
nextcloud:masterfrom
Lenart12:master
Open

fix(libsync): set read-only permissions after rename, not before#10213
Lenart12 wants to merge 1 commit into
nextcloud:masterfrom
Lenart12:master

Conversation

@Lenart12

Copy link
Copy Markdown

setFileReadOnly() on Windows adds an ACCESS_DENIED_ACE for BUILTIN\Users
to the file's DACL. Applying this to the temp file before renaming it to
its final destination blocks the rename, because Windows requires Delete
access on the source path to complete the operation. This caused a
persistent Error 5 (Access Denied) retry loop for read-only shares and
Talk attachments on Windows.

Move the permission block to after uncheckedRenameReplace() succeeds,
targeting the final filename instead of _tmpFile.fileName().

Fixes: #9885

Copilot AI review requested due to automatic review settings June 18, 2026 20:35
setFileReadOnly() on Windows adds an ACCESS_DENIED_ACE for BUILTIN\Users
to the file's DACL. Applying this to the temp file before renaming it to
its final destination blocks the rename, because Windows requires Delete
access on the source path to complete the operation. This caused a
persistent Error 5 (Access Denied) retry loop for read-only shares and
Talk attachments on Windows.

Move the permission block to after uncheckedRenameReplace() succeeds,
targeting the final filename instead of _tmpFile.fileName().

Fixes: nextcloud#9885
Signed-off-by: Lenart Kos <koslenart@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a Windows-specific download failure where applying “read-only” ACLs to the temporary download file prevents the subsequent rename/replace operation, causing persistent Error 5 (Access Denied) retry loops for read-only shares (including Talk attachments). It does so by moving the read-only permission application to after the final rename succeeds.

Changes:

  • Move setFileReadOnly*() calls from the temporary file (_tmpFile.fileName()) to the final destination path (filename) after FileSystem::uncheckedRenameReplace() succeeds.
  • Ensure the final file is un-hidden (setFileHidden(filename, false)) before applying the read-only/read-write decision.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1274 to +1282
if (_item->_locked == SyncFileItem::LockStatus::LockedItem && (_item->_lockOwnerType != SyncFileItem::LockOwnerType::UserLock || _item->_lockOwnerId != propagator()->account()->davUser())) {
qCDebug(lcPropagateDownload()) << filename << "file is locked: making it read only";
FileSystem::setFileReadOnly(filename, true);
} else {
qCDebug(lcPropagateDownload()) << filename << "file is not locked: making it"
<< ((!_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) ? "read only"
: "read write");
FileSystem::setFileReadOnlyWeak(filename, (!_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite)));
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This condition was moved as-is, so the divergence from updateMetadata() is pre-existing. And updateMetadata() runs right after on the same path with the full UserLock/TokenLock check, so it overwrites this block anyway. Behavior is unchanged. Aligning the checks is a fine cleanup but out of scope here.

Comment on lines +1274 to +1282
if (_item->_locked == SyncFileItem::LockStatus::LockedItem && (_item->_lockOwnerType != SyncFileItem::LockOwnerType::UserLock || _item->_lockOwnerId != propagator()->account()->davUser())) {
qCDebug(lcPropagateDownload()) << filename << "file is locked: making it read only";
FileSystem::setFileReadOnly(filename, true);
} else {
qCDebug(lcPropagateDownload()) << filename << "file is not locked: making it"
<< ((!_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite)) ? "read only"
: "read write");
FileSystem::setFileReadOnlyWeak(filename, (!_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite)));
}

@Lenart12 Lenart12 Jun 18, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Real behavioral change, good catch. That said, a case-clash conflict file is a local artifact the user has to resolve manually, so dropping read-only may actually be preferable. @ maintainer is read-only intended on these files? If so, the fix belongs in createCaseClashConflict() after its rename, not in downloadFinished().

@Lenart12

Copy link
Copy Markdown
Author

I checked Copilot suggestions, I think it might need some maintainer input though.

@Lenart12

Copy link
Copy Markdown
Author

Also I have checked the sync code paths and in theory all cases should handle an existing read-only temp file, so this bug should be "self healing" without any additional changes.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Windows Error 5: Access is denied on downloads in Talk.

2 participants