Repository: wagtail/wagtail

Issue:
OSError when image file is missing
If an image entry exists within Wagtail for which the cosponsoring filesystem path does not exist an `OSError` is thrown.

```
Traceback (most recent call last):
  File "/srv/django/site/env/lib/python2.7/site-packages/django/core/handlers/base.py", line 111, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/srv/django/site/env/lib/python2.7/site-packages/wagtail/wagtailimages/views/images.py", line 126, in edit
    'url_generator_enabled': url_generator_enabled,
  File "/srv/django/site/env/lib/python2.7/site-packages/django/shortcuts.py", line 50, in render
    return HttpResponse(loader.render_to_string(*args, **kwargs),
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/loader.py", line 178, in render_to_string
    return t.render(context_instance)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 148, in render
    return self._render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 142, in _render
    return self.nodelist.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 844, in render
    bit = self.render_node(node, context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 858, in render_node
    return node.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/loader_tags.py", line 126, in render
    return compiled_parent._render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 142, in _render
    return self.nodelist.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 844, in render
    bit = self.render_node(node, context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 858, in render_node
    return node.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/loader_tags.py", line 126, in render
    return compiled_parent._render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 142, in _render
    return self.nodelist.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 844, in render
    bit = self.render_node(node, context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 858, in render_node
    return node.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/loader_tags.py", line 126, in render
    return compiled_parent._render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 142, in _render
    return self.nodelist.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 844, in render
    bit = self.render_node(node, context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 858, in render_node
    return node.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/loader_tags.py", line 65, in render
    result = block.nodelist.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 844, in render
    bit = self.render_node(node, context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 858, in render_node
    return node.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/loader_tags.py", line 65, in render
    result = block.nodelist.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 844, in render
    bit = self.render_node(node, context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 858, in render_node
    return node.render(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 898, in render
    output = self.filter_expression.resolve(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 596, in resolve
    obj = self.var.resolve(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 734, in resolve
    value = self._resolve_lookup(context)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/template/base.py", line 770, in _resolve_lookup
    current = getattr(current, bit)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/db/models/fields/files.py", line 74, in _get_size
    return self.storage.size(self.name)
  File "/srv/django/site/env/lib/python2.7/site-packages/django/core/files/storage.py", line 285, in size
    return os.path.getsize(self.path(name))
  File "/srv/django/site/env/lib/python2.7/genericpath.py", line 49, in getsize
    return os.stat(filename).st_size
OSError: [Errno 2] No such file or directory: '/srv/django/site/media/original_images/img-17686908.jpg'
```



Discussion:
We fixed a similar issue in `0.8.3` (see: https://github.com/torchbox/wagtail/blob/e0fe9af555e4e9f9b731c52f69d2c86746c02ffc/CHANGELOG.txt#L46). Are you using the latest version?

I'm on the master branch as the MySQL migration fix hasn't made it to pypi yet.

Just had a closer look at your traceback and I think I've spotted the problem.

It looks like you're not using Wagtails `{% image %}` tag for rendering the image which would usually catch this exception.

If you'd like to show your image at it's original size, I'd recommend using the `original` filter like so: `{% image myimage original %}`. (http://docs.wagtail.io/en/latest/core_components/images/index.html#using-images-in-templates)

I should have been more explicit in my bug report. This occurs in the Wagtail backend when the file system path to the uploaded original image doesn't exist any more.

**Steps to reproduce**
- Upload an image in the wagtail backend
- rm the file
- View the file page in the Wagtail backend

This prevents deletion of the related db entries via the Wagtail interface without re-adding a file at that path.

Ohh, apologies for the confusion.

I'll look into this further

I can confirm that this is a bug introduced in Wagtail 0.9.

It appears that we are now reading the file to check its size but are not checking that it exists first. https://github.com/torchbox/wagtail/commit/9fe6f04d7b11a0d3c7fc6c850e290b94bd29f126

Is this a meaningful bug? I'm not particularly convinced :-) We have fallback behaviour on front-end templates for missing image files, as a convenience to developers - so that they can pull down live database snapshots to their local development version and still be able to render templates. But if you're going in to the image admin area and editing actual image records, there's a reasonable expectation that the image ought to exist, and it's not unreasonable for Wagtail to throw an error if it doesn't.

I think it's a bug.

Wagtail `0.8.x` didn't do it and Wagtail `0.9.x` only does it due to an unintended side effect of getting the filesize of the nonexistant image in the template.

If we want Wagtail to crash in this case, I think we should be explicity telling Wagtail to crash instead of relying on `file.size` to crash for us. And add a test for it.

It's a bug for sure but not a major one. It would just be more convenient to be able to delete the image entry via the wagtail UI in the case of a missing file rather than either having to resort to `./manage.py shell`, direct database access or putting a dummy file in place.

@kaedroho Well, that's just the nature of undefined behaviour... just because something worked by chance in the past doesn't mean that we should commit to it continuing to work, or failing in a predictable way. And I think there's merit in leaving behaviour undefined sometimes, providing it doesn't interfere with normal use of the system - it means there's one fewer edge case to account for when we refactor the code in future.

Having said that - if you're willing to fix this and commit to maintaining a particular behaviour in future, go ahead!

@gasman I think it depends on whether you want to program defensively or not. Generally my principle with regards to I/O is always presume it's going to fail in some edge cases and be as defensive as possible.


Resolving patch:
diff --git a/wagtail/wagtailimages/views/images.py b/wagtail/wagtailimages/views/images.py
--- a/wagtail/wagtailimages/views/images.py
+++ b/wagtail/wagtailimages/views/images.py
@@ -119,10 +119,18 @@ def edit(request, image_id):
     except NoReverseMatch:
         url_generator_enabled = False
 
+    # Get file size
+    try:
+        filesize = image.file.size
+    except OSError:
+        # File doesn't exist
+        filesize = None
+
     return render(request, "wagtailimages/images/edit.html", {
         'image': image,
         'form': form,
         'url_generator_enabled': url_generator_enabled,
+        'filesize': filesize,
     })
 
 


Test patch:
diff --git a/wagtail/wagtailimages/tests/test_admin_views.py b/wagtail/wagtailimages/tests/test_admin_views.py
--- a/wagtail/wagtailimages/tests/test_admin_views.py
+++ b/wagtail/wagtailimages/tests/test_admin_views.py
@@ -121,6 +121,13 @@ def test_edit(self):
         image = Image.objects.get(id=self.image.id)
         self.assertEqual(image.title, "Edited")
 
+    def test_with_missing_image_file(self):
+        self.image.file.delete(False)
+
+        response = self.get()
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'wagtailimages/images/edit.html')
+
 
 class TestImageDeleteView(TestCase, WagtailTestUtils):
     def setUp(self):