Repository: jschneier/django-storages

Issue:
Dropbox - path in Django field is broken in 1.13.1+
I have Django installation with Dropbox storage for FileField content. With this settings it strips 2 symbols in path: 
```
DROPBOX_ROOT_PATH = '/app.pbb.foo/'
DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage'
```

for ex., for field
```
    file = models.FileField(
        upload_to='backups',
        blank=True,
        validators=[backup_file_validator]
    )
```
when I tried to store file `site55-full_2023_02_07_13_13_49.tar.gz` I've got `ckups/site55-full_2023_02_07_13_13_49.tar.gz` in FileField instead of `backups/site55-full_2023_02_07_13_13_49.tar.gz` and of course file is unavailable in app.

with 1.9.1 all is OK; 1.10.0-1.13.0 raise exception SuspiciousFileOperation: Detected path traversal attempt due to leading slash in path.

but with
```
DROPBOX_ROOT_PATH = '/app.poo.foo/'
```
first 2 symbols aren't stripped.

Python 3.8, Django 3.2, dropbox 11.36.0


Discussion:
Is the issue with `DROPBOX_ROOT_PATH = '/app.poo.foo/'` or something else stripping the beginning of `backups`?
yes. I've got `ckups/....` instead of `backups/...` in model
What is the content of `backup_file_validator`? I'm struggling to see how this app causes this issue but I can't say it's impossible.
just validation of file content (check that it's .tar.gz file).

Resolving patch:
diff --git a/storages/backends/dropbox.py b/storages/backends/dropbox.py
--- a/storages/backends/dropbox.py
+++ b/storages/backends/dropbox.py
@@ -33,6 +33,12 @@ class DropboxStorageException(Exception):
 DropBoxStorageException = DropboxStorageException
 
 
+def removeprefix(prefix, name):
+    if name.startswith(prefix):
+        name = name[len(prefix):]
+    return name
+
+
 class DropboxFile(File):
     def __init__(self, name, storage):
         self.name = name
@@ -172,7 +178,7 @@ def _save(self, name, content):
         content.close()
         # .save() validates the filename isn't absolute but Dropbox requires an absolute filename.
         # Work with the absolute name internally but strip it off before passing up-the-chain.
-        return name.lstrip(self.root_path)
+        return removeprefix(self.root_path, name).lstrip('/')
 
     def _chunked_upload(self, content, dest_path):
         upload_session = self.client.files_upload_session_start(


Test patch:
diff --git a/tests/test_dropbox.py b/tests/test_dropbox.py
--- a/tests/test_dropbox.py
+++ b/tests/test_dropbox.py
@@ -197,9 +197,11 @@ def test_jailed(self, *args):
     @mock.patch('dropbox.Dropbox.files_upload', return_value='foo')
     @mock.patch('dropbox.Dropbox.files_get_metadata', return_value=None)
     def test_saves(self, *args):
-        self.storage = dropbox.DropboxStorage('foo', root_path='/bar')
-        name = self.storage.save('xyz', File(io.BytesIO(b'abc'), 'def'))
-        self.assertEqual(name, 'xyz')
+        self.storage = dropbox.DropboxStorage('foo', root_path='/app.qoo.foo/')
+        for filename in ['xyz', 'quark']:
+            with self.subTest(filename=filename):
+                name = self.storage.save(filename, File(io.BytesIO(b'abc'), 'def'))
+                self.assertEqual(name, filename)
 
     def test_suspicious(self, *args):
         self.storage = dropbox.DropboxStorage('foo', root_path='/bar')