⚠️ This issue respects the following points: ⚠️
This was investigated, fixed on my local system and following issue report text created by Deepseek v4 Pro. Apologizes for anything that could be not correct. I'm available to give you any further detail about my setup if needed.
Also, it seems that server-side encryption by Nextcloud application is discouraged when using remote NFS mount, while is acceptable when using local storage mounts. If so, I would only ask you to update documentation to clearly state it and close this issue without fixing it, would be fine by me.
Bug description
Chunked uploads (>10 MB) fail with Sabre\DAV\Exception\Forbidden (403) when the data directory is on NFS with server-side encryption enabled. This affects both the web UI and the desktop sync client. Additionally, when encryption is enabled, the same files become undownloadable (500) after a failed upload attempt because encryption key directories are left in orphaned .part state.
Affected versions
33.0.4.1 (and likely all stable branches back to at least 29)
Environment
- Deployment: Docker Swarm (
nextcloud:apache official image)
- Data directory: NFSv4.1 (
rw,noatime,sync,vers=4.1,local_lock=none)
- Reverse proxy: Traefik service in Docker Swarm
- PHP upload limit: 10G
- File locking: Redis
- Server-side encryption: enabled
Root causes (4 distinct issues)
1. Directory::createFile() — isCreatable() blocks chunk PUT (Forbidden 403)
apps/dav/lib/Connector/Sabre/Directory.php:103
public function createFile($name, $data = null) {
try {
// This returns false from web requests (true from CLI):
if (!$this->fileView->isCreatable($this->path)) {
throw new \Sabre\DAV\Exception\Forbidden();
}
The fileView->isCreatable() call relies on cached permission bits that
differ between web and CLI contexts. The directory permissions (mode 23)
are technically correct for the oc_share_folder mask but the View's
permission cache can be stale or populated from a different code path.
Removing this check (permissions are already verified at the mount/setup
layer) allows chunk PUTs to proceed.
This is the same symptom reported in #46170
(load-balanced environment), but the root cause there was different (stale
cache between nodes). The underlying fragility of isCreatable() affects
both scenarios.
2. UploadFolder::createFile() — .part rename fails on NFS
apps/dav/lib/Upload/UploadFolder.php:32
Parent Directory::createFile() calls $storage->moveFromStorage() to rename
the .ocTransferId*.part temporary file to the final chunk name. On NFS,
moveFromStorage returns false with "source directory is not writable"
because the View folder is shared between APC and Redis and the storage path
check during rename disagrees on permissions.
Writing directly via $this->storage->file_put_contents() avoids the .part
rename entirely and works correctly on NFS.
3. Directory::delete() / FutureFile::delete() — cleanup blocked by isDeletable()
apps/dav/lib/Connector/Sabre/Directory.php:271 and
apps/dav/lib/Upload/FutureFile.php:81
The upload session directory has permissions 23 (read+write+share, but no
DELETE=8), so isDeletable() returns false and Directory::delete() throws
Forbidden during cleanup. Wrapping the delete in try/catch and removing the
isDeletable() guard allows the upload assembly to complete.
4. Encryption\Keys\Storage::renameKeys() — no fallback when rename fails on NFS
lib/private/Encryption/Keys/Storage.php:345
After file_put_contents, encryption keys are written to a directory named
after the .ocTransferId*.part file. renameKeys() is called to rename the
key directory to the final filename. On NFS, $this->view->rename() can
fail silently, leaving the keys in the orphaned .part directory. When
downloading the file, getFileKey() cannot find the keys and returns empty
string → 500 Internal Server Error.
Proposed fixes
Fix 1 — apps/dav/lib/Connector/Sabre/Directory.php
Remove the isCreatable() guard in createFile():
public function createFile($name, $data = null) {
try {
- if (!$this->fileView->isCreatable($this->path)) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
$this->fileView->verifyPath($this->path, $name);
Use $this->info->isCreatable() (file info metadata) instead of
$this->fileView->isCreatable() (View permission cache) in moveInto().
Remove the isDeletable() guard in delete() to allow cleanup of
session directories with permission bits that don't include DELETE=8.
Fix 2 — apps/dav/lib/Upload/UploadFolder.php
Write chunk data directly via storage, bypassing the .part rename:
public function createFile($name, $data = null) {
- return parent::createFile($name, $data);
+ $content = stream_get_contents($data);
+ fclose($data);
+ $this->storage->file_put_contents($this->getName() . '/' . $name, $content);
+ return '"' . $this->getETag() . '"';
}
Fix 3 — apps/dav/lib/Upload/FutureFile.php
Wrap delete in try/catch so cleanup failure does not block assembly:
public function delete() {
- $this->root->delete();
+ try { $this->root->delete(); } catch (\Exception $e) {}
}
Fix 4 — lib/private/Encryption/Keys/Storage.php
Add copy fallback in renameKeys() when rename fails:
- $this->view->rename($sourcePath, $targetPath);
+ $renameResult = @$this->view->rename($sourcePath, $targetPath);
+ if (!$renameResult || !$this->view->file_exists($targetPath)) {
+ if ($this->view->is_dir($sourcePath)) {
+ $dh = $this->view->opendir($sourcePath);
+ if ($dh) {
+ while (($file = readdir($dh)) !== false) {
+ if ($file !== '.' && $file !== '..') {
+ $this->view->copy($sourcePath . '/' . $file,
+ $targetPath . '/' . $file);
+ }
+ }
+ closedir($dh);
+ }
+ }
+ }
return true;
Add fallback in getFileKey() to search orphaned .ocTransferId*.part
key directories when the key cannot be found at the expected path:
+ if ($key === '') {
+ $parentKeyDir = dirname(dirname(
+ $this->util->getFileKeyDir($encryptionModuleId, $realFile)));
+ if ($this->view->is_dir($parentKeyDir)) {
+ $dh = $this->view->opendir($parentKeyDir);
+ if ($dh) {
+ while (($entry = readdir($dh)) !== false) {
+ if (strpos($entry, '.ocTransferId') !== false
+ && str_ends_with($entry, '.part')) {
+ $keyResult = $this->getKey(
+ $parentKeyDir . '/' . $entry . '/'
+ . $encryptionModuleId . '/' . $keyId);
+ if (!empty($keyResult['key'])) {
+ $key = $keyResult['key'];
+ break;
+ }
+ }
+ }
+ closedir($dh);
+ }
+ }
+ }
Steps to reproduce
- Deploy Nextcloud 33.0.4.1 with data directory on NFSv4
- Enable server-side encryption and Redis file locking
- Set upload chunk size to 10 MB (default)
- Upload a file >10 MB (the hardcoded threshold that triggers chunked upload) via web UI or desktop client
- Observe:
PUT /remote.php/dav/uploads/... returns 403
- If encryption keys end up in
.part directories: GET /remote.php/dav/files/... returns 500
Expected behavior
Test results after applying fixes
- MKCOL → 201
- PUT chunks (10x100MB) → all 201
- MOVE assembly → 201
- GET/download → 200 (924 MB verified by desktop client sync)
- Encryption keys correctly created at final filenames (not
.part)
- Both web UI upload and desktop client sync working
Nextcloud Server version
33
Operating system
Debian/Ubuntu
PHP engine version
8.4.21
Web server
Apache (supported)
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
I don't know, I noticed this issue randomly while noticing to have lost some file (even old ones). I noticed when I moved some subfolder under another folder and was not able to read a lot of files anymore due to .part keys named files
Are you using the Nextcloud Server Encryption module?
Encryption is Enabled
What user-backends are you using?
Configuration report
{
"system": {
"htaccess.RewriteBase": "\/",
"filelocking.enabled": true,
"memcache.local": "\\OC\\Memcache\\APCu",
"memcache.locking": "\\OC\\Memcache\\Redis",
"memcache.distributed": "\\OC\\Memcache\\Redis",
"redis": {
"host": "***REMOVED SENSITIVE VALUE***",
"port": 6379,
"password": "***REMOVED SENSITIVE VALUE***"
},
"apps_paths": [
{
"path": "\/var\/www\/html\/apps",
"url": "\/apps",
"writable": false
},
{
"path": "\/var\/www\/html\/custom_apps",
"url": "\/custom_apps",
"writable": true
}
],
"instanceid": "***REMOVED SENSITIVE VALUE***",
"passwordsalt": "***REMOVED SENSITIVE VALUE***",
"secret": "***REMOVED SENSITIVE VALUE***",
"trusted_domains": {
"1": "nextcloud.***REMOVED SENSITIVE VALUE***",
"2": "nextcloud.***REMOVED SENSITIVE VALUE***"
},
"datadirectory": "***REMOVED SENSITIVE VALUE***",
"dbtype": "mysql",
"version": "33.0.4.1",
"dbname": "***REMOVED SENSITIVE VALUE***",
"dbhost": "***REMOVED SENSITIVE VALUE***",
"dbport": "",
"dbtableprefix": "oc_",
"dbuser": "***REMOVED SENSITIVE VALUE***",
"dbpassword": "***REMOVED SENSITIVE VALUE***",
"default_phone_region": ".it",
"installed": true,
"mail_smtpmode": "smtp",
"mail_sendmailmode": "smtp",
"mail_smtpauth": 1,
"mail_smtpauthtype": "LOGIN",
"mail_from_address": "***REMOVED SENSITIVE VALUE***",
"mail_domain": "***REMOVED SENSITIVE VALUE***",
"mail_smtphost": "***REMOVED SENSITIVE VALUE***",
"mail_smtpport": "587",
"mail_smtpname": "***REMOVED SENSITIVE VALUE***",
"mail_smtppassword": "***REMOVED SENSITIVE VALUE***",
"maintenance": false,
"maintenance_window_start": 1,
"mysql.utf8mb4": true,
"loglevel": 0,
"logtimezone": "Europe\/Rome",
"theme": "",
"data-fingerprint": "640ec60ef2eec9c39d897aa683a0a33d",
"post_max_size": "10G",
"upload_max_filesize": "10G",
"app_install_overwrite": {
"0": "workflow_media_converter",
"1": "checksum",
"2": "backup",
"4": "duplicatefinder",
"5": "ransomware_protection",
"6": "files_external_onedrive",
"7": "extract",
"8": "files_rightclick",
"9": "twofactor_nextcloud_notification",
"13": "epubviewer"
},
"enable_previews": true,
"enabledPreviewProviders": [
"OC\\Preview\\PNG",
"OC\\Preview\\JPEG",
"OC\\Preview\\GIF",
"OC\\Preview\\BMP",
"OC\\Preview\\XBitmap",
"OC\\Preview\\MP3",
"OC\\Preview\\TXT",
"OC\\Preview\\MarkDown",
"OC\\Preview\\OpenDocument",
"OC\\Preview\\Krita",
"OC\\Preview\\HEIC"
],
"memories.exiftool": "\/var\/www\/html\/custom_apps\/memories\/bin-ext\/exiftool-aarch64-musl",
"memories.vod.path": "\/var\/www\/html\/custom_apps\/memories\/bin-ext\/go-vod-aarch64",
"overwrite.cli.url": "https:\/\/nextcloud.***REMOVED SENSITIVE VALUE***",
"twofactor_enforced": "true",
"twofactor_enforced_groups": [],
"twofactor_enforced_excluded_groups": [],
"trusted_proxies": "***REMOVED SENSITIVE VALUE***",
"config_preset": 2
}
}
List of activated Apps
Enabled:
- activity: 6.0.0
- admin_audit: 1.23.0
- bruteforcesettings: 6.0.0
- checksum: 2.1.2
- cloud_federation_api: 1.17.0
- comments: 1.23.0
- dav: 1.36.0
- encryption: 2.21.0
- epubviewer: 1.9.2
- federatedfilesharing: 1.23.0
- files: 2.5.0
- files_downloadlimit: 5.1.0
- files_pdfviewer: 6.0.0
- files_reminders: 1.6.0
- files_sharing: 1.25.2
- files_versions: 1.26.0
- firstrunwizard: 6.0.0
- guests: 4.7.5
- logreader: 6.0.0
- lookup_server_connector: 1.21.0
- nextcloud_announcements: 5.0.0
- notifications: 6.0.0
- oauth2: 1.21.0
- password_policy: 5.0.0
- privacy: 5.0.0
- profile: 1.2.0
- provisioning_api: 1.23.0
- recommendations: 6.0.0
- related_resources: 4.0.0
- serverinfo: 5.0.0
- settings: 1.16.0
- sharebymail: 1.23.0
- suspicious_login: 11.0.0
- systemtags: 1.23.0
- text: 7.0.1
- theming: 2.8.0
- twofactor_backupcodes: 1.22.0
- twofactor_totp: 15.0.0
- updatenotification: 1.23.0
- user_status: 1.13.0
- viewer: 6.0.0
- webhook_listeners: 1.5.0
- workflowengine: 2.15.0
Disabled:
- app_api: 33.0.0 (installed 32.0.0)
- circles: 33.0.0 (installed 31.0.0)
- contactsinteraction: 1.14.1 (installed 1.12.0)
- dashboard: 7.13.0 (installed 7.11.0)
- federation: 1.23.0 (installed 1.21.0)
- files_external: 1.25.1 (installed 1.23.0)
- files_trashbin: 1.23.0 (installed 1.23.0)
- files_zip: 2.3.0 (installed 2.3.0)
- imageconverter: 2.2.0 (installed 2.2.0)
- photos: 6.0.0 (installed 4.0.0-dev.1)
- ransomware_protection: 1.14.0 (installed 1.14.0)
- richdocuments: 10.1.3 (installed 10.1.3)
- support: 5.0.0 (installed 1.5.0)
- survey_client: 5.0.0 (installed 1.10.0)
- twofactor_nextcloud_notification: 7.0.0 (installed 5.0.0)
- user_ldap: 1.24.0
- weather_status: 1.13.0 (installed 1.1.0)
Nextcloud Signing status
No errors have been found.
Nextcloud Logs
`nextcloud.log` — download fails because keys are not at expected path:
{"reqId":"Zf3W...","level":3,"time":"2026-06-02T16:10:00+02:00","user":"stefano","app":"webdav","method":"GET","url":"/remote.php/dav/files/stefano/Documents/test.pdf","message":"Cannot decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.","exception":{"Exception":"OC\\Encryption\\Exceptions\\DecryptionFailedException","File":"/var/www/html/apps/encryption/lib/Crypto/Encryption.php","Line":398}}
Orphaned key directory found on filesystem (valid key, wrong path):
files_encryption/keys/files/Documents/test.pdf/OC_DEFAULT_MODULE/ ← getFileKey looks here → empty
files_encryption/keys/files/Documents/.ocTransferId123456789.part/OC_DEFAULT_MODULE/master_f9b1621e.shareKey ← actual key is here
cURL verification — GET returns 500 when keys are in `.part` dir:
$ curl -u user:pass -X GET https://cloud.example.com/remote.php/dav/files/user/Documents/test.pdf
HTTP/1.1 500 Internal Server Error
After manually moving the `.part` key directory to the correct name, the
same cURL returns 200 — confirming the root cause is orphaned key paths.
Additional info
- Issue #46170 reports
the same 403 symptom but from a different trigger (load-balanced
environment). Both share the fragile isCreatable() check.
- Issue #40193 reports
decryption failures on older files — the getFileKey() fallback proposed
here may help with that class of problems as well.
ChunkingV2Plugin was not modified; the V1 fallback path works correctly
with these patches (V2 requires S3-compatible object storage).
- The fixes are intended as one-time patches for Docker-based deployments
and do not survive Nextcloud upgrades without reapplication.
This was investigated, fixed on my local system and following issue report text created by Deepseek v4 Pro. Apologizes for anything that could be not correct. I'm available to give you any further detail about my setup if needed.
Also, it seems that server-side encryption by Nextcloud application is discouraged when using remote NFS mount, while is acceptable when using local storage mounts. If so, I would only ask you to update documentation to clearly state it and close this issue without fixing it, would be fine by me.
Bug description
Chunked uploads (>10 MB) fail with
Sabre\DAV\Exception\Forbidden(403) when the data directory is on NFS with server-side encryption enabled. This affects both the web UI and the desktop sync client. Additionally, when encryption is enabled, the same files become undownloadable (500) after a failed upload attempt because encryption key directories are left in orphaned.partstate.Affected versions
33.0.4.1 (and likely all stable branches back to at least 29)
Environment
nextcloud:apacheofficial image)rw,noatime,sync,vers=4.1,local_lock=none)Root causes (4 distinct issues)
1.
Directory::createFile()—isCreatable()blocks chunk PUT (Forbidden 403)apps/dav/lib/Connector/Sabre/Directory.php:103The
fileView->isCreatable()call relies on cached permission bits thatdiffer between web and CLI contexts. The directory permissions (mode 23)
are technically correct for the
oc_share_foldermask but the View'spermission cache can be stale or populated from a different code path.
Removing this check (permissions are already verified at the mount/setup
layer) allows chunk PUTs to proceed.
This is the same symptom reported in #46170
(load-balanced environment), but the root cause there was different (stale
cache between nodes). The underlying fragility of
isCreatable()affectsboth scenarios.
2.
UploadFolder::createFile()—.partrename fails on NFSapps/dav/lib/Upload/UploadFolder.php:32Parent
Directory::createFile()calls$storage->moveFromStorage()to renamethe
.ocTransferId*.parttemporary file to the final chunk name. On NFS,moveFromStoragereturnsfalsewith "source directory is not writable"because the View folder is shared between APC and Redis and the storage path
check during rename disagrees on permissions.
Writing directly via
$this->storage->file_put_contents()avoids the.partrename entirely and works correctly on NFS.
3.
Directory::delete()/FutureFile::delete()— cleanup blocked byisDeletable()apps/dav/lib/Connector/Sabre/Directory.php:271andapps/dav/lib/Upload/FutureFile.php:81The upload session directory has permissions 23 (read+write+share, but no
DELETE=8), so
isDeletable()returns false andDirectory::delete()throwsForbidden during cleanup. Wrapping the delete in try/catch and removing the
isDeletable()guard allows the upload assembly to complete.4.
Encryption\Keys\Storage::renameKeys()— no fallback when rename fails on NFSlib/private/Encryption/Keys/Storage.php:345After
file_put_contents, encryption keys are written to a directory namedafter the
.ocTransferId*.partfile.renameKeys()is called to rename thekey directory to the final filename. On NFS,
$this->view->rename()canfail silently, leaving the keys in the orphaned
.partdirectory. Whendownloading the file,
getFileKey()cannot find the keys and returns emptystring → 500 Internal Server Error.
Proposed fixes
Fix 1 —
apps/dav/lib/Connector/Sabre/Directory.phpRemove the
isCreatable()guard increateFile():public function createFile($name, $data = null) { try { - if (!$this->fileView->isCreatable($this->path)) { - throw new \Sabre\DAV\Exception\Forbidden(); - } $this->fileView->verifyPath($this->path, $name);Use
$this->info->isCreatable()(file info metadata) instead of$this->fileView->isCreatable()(View permission cache) inmoveInto().Remove the
isDeletable()guard indelete()to allow cleanup ofsession directories with permission bits that don't include DELETE=8.
Fix 2 —
apps/dav/lib/Upload/UploadFolder.phpWrite chunk data directly via storage, bypassing the
.partrename:public function createFile($name, $data = null) { - return parent::createFile($name, $data); + $content = stream_get_contents($data); + fclose($data); + $this->storage->file_put_contents($this->getName() . '/' . $name, $content); + return '"' . $this->getETag() . '"'; }Fix 3 —
apps/dav/lib/Upload/FutureFile.phpWrap delete in try/catch so cleanup failure does not block assembly:
public function delete() { - $this->root->delete(); + try { $this->root->delete(); } catch (\Exception $e) {} }Fix 4 —
lib/private/Encryption/Keys/Storage.phpAdd copy fallback in
renameKeys()when rename fails:Add fallback in
getFileKey()to search orphaned.ocTransferId*.partkey directories when the key cannot be found at the expected path:
Steps to reproduce
PUT /remote.php/dav/uploads/...returns 403.partdirectories:GET /remote.php/dav/files/...returns 500Expected behavior
Test results after applying fixes
.part)Nextcloud Server version
33
Operating system
Debian/Ubuntu
PHP engine version
8.4.21
Web server
Apache (supported)
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
I don't know, I noticed this issue randomly while noticing to have lost some file (even old ones). I noticed when I moved some subfolder under another folder and was not able to read a lot of files anymore due to .part keys named files
Are you using the Nextcloud Server Encryption module?
Encryption is Enabled
What user-backends are you using?
Configuration report
{ "system": { "htaccess.RewriteBase": "\/", "filelocking.enabled": true, "memcache.local": "\\OC\\Memcache\\APCu", "memcache.locking": "\\OC\\Memcache\\Redis", "memcache.distributed": "\\OC\\Memcache\\Redis", "redis": { "host": "***REMOVED SENSITIVE VALUE***", "port": 6379, "password": "***REMOVED SENSITIVE VALUE***" }, "apps_paths": [ { "path": "\/var\/www\/html\/apps", "url": "\/apps", "writable": false }, { "path": "\/var\/www\/html\/custom_apps", "url": "\/custom_apps", "writable": true } ], "instanceid": "***REMOVED SENSITIVE VALUE***", "passwordsalt": "***REMOVED SENSITIVE VALUE***", "secret": "***REMOVED SENSITIVE VALUE***", "trusted_domains": { "1": "nextcloud.***REMOVED SENSITIVE VALUE***", "2": "nextcloud.***REMOVED SENSITIVE VALUE***" }, "datadirectory": "***REMOVED SENSITIVE VALUE***", "dbtype": "mysql", "version": "33.0.4.1", "dbname": "***REMOVED SENSITIVE VALUE***", "dbhost": "***REMOVED SENSITIVE VALUE***", "dbport": "", "dbtableprefix": "oc_", "dbuser": "***REMOVED SENSITIVE VALUE***", "dbpassword": "***REMOVED SENSITIVE VALUE***", "default_phone_region": ".it", "installed": true, "mail_smtpmode": "smtp", "mail_sendmailmode": "smtp", "mail_smtpauth": 1, "mail_smtpauthtype": "LOGIN", "mail_from_address": "***REMOVED SENSITIVE VALUE***", "mail_domain": "***REMOVED SENSITIVE VALUE***", "mail_smtphost": "***REMOVED SENSITIVE VALUE***", "mail_smtpport": "587", "mail_smtpname": "***REMOVED SENSITIVE VALUE***", "mail_smtppassword": "***REMOVED SENSITIVE VALUE***", "maintenance": false, "maintenance_window_start": 1, "mysql.utf8mb4": true, "loglevel": 0, "logtimezone": "Europe\/Rome", "theme": "", "data-fingerprint": "640ec60ef2eec9c39d897aa683a0a33d", "post_max_size": "10G", "upload_max_filesize": "10G", "app_install_overwrite": { "0": "workflow_media_converter", "1": "checksum", "2": "backup", "4": "duplicatefinder", "5": "ransomware_protection", "6": "files_external_onedrive", "7": "extract", "8": "files_rightclick", "9": "twofactor_nextcloud_notification", "13": "epubviewer" }, "enable_previews": true, "enabledPreviewProviders": [ "OC\\Preview\\PNG", "OC\\Preview\\JPEG", "OC\\Preview\\GIF", "OC\\Preview\\BMP", "OC\\Preview\\XBitmap", "OC\\Preview\\MP3", "OC\\Preview\\TXT", "OC\\Preview\\MarkDown", "OC\\Preview\\OpenDocument", "OC\\Preview\\Krita", "OC\\Preview\\HEIC" ], "memories.exiftool": "\/var\/www\/html\/custom_apps\/memories\/bin-ext\/exiftool-aarch64-musl", "memories.vod.path": "\/var\/www\/html\/custom_apps\/memories\/bin-ext\/go-vod-aarch64", "overwrite.cli.url": "https:\/\/nextcloud.***REMOVED SENSITIVE VALUE***", "twofactor_enforced": "true", "twofactor_enforced_groups": [], "twofactor_enforced_excluded_groups": [], "trusted_proxies": "***REMOVED SENSITIVE VALUE***", "config_preset": 2 } }List of activated Apps
Nextcloud Signing status
Nextcloud Logs
Additional info
the same 403 symptom but from a different trigger (load-balanced
environment). Both share the fragile
isCreatable()check.decryption failures on older files — the
getFileKey()fallback proposedhere may help with that class of problems as well.
ChunkingV2Pluginwas not modified; the V1 fallback path works correctlywith these patches (V2 requires S3-compatible object storage).
and do not survive Nextcloud upgrades without reapplication.