Repository: jschneier/django-storages

Issue:
sftp.SFTPStorageFile doesn't properly call it's parent __init__
Django's file object assumes it has a `.name` attribute, like in the `__bool__` method here: https://github.com/django/django/blob/1.11.12/django/core/files/base.py#L31-L32

It does so, because it sets this attribute in its constructor here: https://github.com/django/django/blob/1.11.12/django/core/files/base.py#L19-L21

However, the `sftp.SFTPStorageFile` object does not call that constructor:  https://github.com/django/django/blob/1.11.12/django/core/files/base.py#L19-L21

Simply adding in `super(SFTPStorageFile, self).__init__(file=self.file, name=self._name)` at the end of that method solves the issue. I'm not setup to make a PR at the moment, or this would also include a PR. I'm happy to make one later today. 


Discussion:
I came across the same issue, and found basically the same solution fiddling around the `__init__`. It seems that you fixed much more with #489 ! Any chance to get it passing the unit tests ? 
I would love to, but I haven't had the time to get them setup and running locally. :(
I have opened the rebased version at #558 to play with the tests, sorry for the delay.
Sorry inadvertently closed this one.

Resolving patch:
diff --git a/storages/backends/sftpstorage.py b/storages/backends/sftpstorage.py
--- a/storages/backends/sftpstorage.py
+++ b/storages/backends/sftpstorage.py
@@ -46,9 +46,6 @@ def __init__(self, host=None, params=None, interactive=None, file_mode=None,
             if root_path is None else root_path
         self._base_url = setting('MEDIA_URL') if base_url is None else base_url
 
-        # for now it's all posix paths.  Maybe someday we'll support figuring
-        # out if the remote host is windows.
-        self._pathmod = posixpath
         self._sftp = None
 
     def _connect(self):
@@ -90,12 +87,8 @@ def sftp(self):
             self._connect()
         return self._sftp
 
-    def _join(self, *args):
-        # Use the path module for the remote host type to join a path together
-        return self._pathmod.join(*args)
-
     def _remote_path(self, name):
-        return self._join(self._root_path, name)
+        return posixpath.join(self._root_path, name)
 
     def _open(self, name, mode='rb'):
         return SFTPStorageFile(name, self, mode)
@@ -117,7 +110,7 @@ def _chown(self, path, uid=None, gid=None):
     def _mkdir(self, path):
         """Create directory, recursing up to create parent dirs if
         necessary."""
-        parent = self._pathmod.dirname(path)
+        parent = posixpath.dirname(path)
         if not self.exists(parent):
             self._mkdir(parent)
         self.sftp.mkdir(path)
@@ -132,7 +125,7 @@ def _save(self, name, content):
         """Save file via SFTP."""
         content.open()
         path = self._remote_path(name)
-        dirname = self._pathmod.dirname(path)
+        dirname = posixpath.dirname(path)
         if not self.exists(dirname):
             self._mkdir(dirname)
 
@@ -148,15 +141,14 @@ def _save(self, name, content):
         return name
 
     def delete(self, name):
-        remote_path = self._remote_path(name)
-        self.sftp.remove(remote_path)
+        try:
+            self.sftp.remove(self._remote_path(name))
+        except IOError:
+            pass
 
     def exists(self, name):
-        # Try to retrieve file info.  Return true on success, false on failure.
-        remote_path = self._remote_path(name)
-
         try:
-            self.sftp.stat(remote_path)
+            self.sftp.stat(self._remote_path(name))
             return True
         except IOError:
             return False
@@ -200,28 +192,28 @@ def url(self, name):
 
 class SFTPStorageFile(File):
     def __init__(self, name, storage, mode):
-        self._name = name
-        self._storage = storage
-        self._mode = mode
-        self._is_dirty = False
+        self.name = name
+        self.mode = mode
         self.file = BytesIO()
+        self._storage = storage
         self._is_read = False
+        self._is_dirty = False
 
     @property
     def size(self):
         if not hasattr(self, '_size'):
-            self._size = self._storage.size(self._name)
+            self._size = self._storage.size(self.name)
         return self._size
 
     def read(self, num_bytes=None):
         if not self._is_read:
-            self.file = self._storage._read(self._name)
+            self.file = self._storage._read(self.name)
             self._is_read = True
 
         return self.file.read(num_bytes)
 
     def write(self, content):
-        if 'w' not in self._mode:
+        if 'w' not in self.mode:
             raise AttributeError("File was opened for read-only access.")
         self.file = BytesIO(content)
         self._is_dirty = True
@@ -229,5 +221,5 @@ def write(self, content):
 
     def close(self):
         if self._is_dirty:
-            self._storage._save(self._name, self)
+            self._storage._save(self.name, self)
         self.file.close()


Test patch: