Merge lp:~jtv/maas/migrate-filestorage into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 560
Proposed branch: lp:~jtv/maas/migrate-filestorage
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 350 lines (+165/-135)
2 files modified
src/maasserver/models/__init__.py (+2/-135)
src/maasserver/models/filestorage.py (+163/-0)
To merge this branch: bzr merge lp:~jtv/maas/migrate-filestorage
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+106127@code.launchpad.net

Commit message

Model migration: FileStorage.

Description of the change

As per the migration plan, which is proceeding uneventfully (yay). This moves FileStorage and its manager into their own module. And I even remembered to “bzr add” the new module this time.

Due to land on the 18th.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/__init__.py'
2--- src/maasserver/models/__init__.py 2012-05-11 08:02:22 +0000
3+++ src/maasserver/models/__init__.py 2012-05-17 09:20:23 +0000
4@@ -20,6 +20,7 @@
5 "get_auth_tokens",
6 "get_db_state",
7 "get_html_display_for_key",
8+ "logger",
9 "Config",
10 "FileStorage",
11 "NODE_TRANSITIONS",
12@@ -31,12 +32,10 @@
13
14 import binascii
15 from cgi import escape
16-from errno import ENOENT
17 from logging import getLogger
18 import os
19 import re
20 from string import whitespace
21-import time
22 from uuid import uuid1
23
24 from django.conf import settings
25@@ -47,12 +46,9 @@
26 PermissionDenied,
27 ValidationError,
28 )
29-from django.core.files.base import ContentFile
30-from django.core.files.storage import FileSystemStorage
31 from django.db import connection
32 from django.db.models import (
33 CharField,
34- FileField,
35 ForeignKey,
36 IntegerField,
37 Manager,
38@@ -82,6 +78,7 @@
39 from maasserver.fields import MACAddressField
40 from maasserver.models.cleansave import CleanSave
41 from maasserver.models.config import Config
42+from maasserver.models.filestorage import FileStorage
43 from maasserver.models.timestampedmodel import TimestampedModel
44 from metadataserver import nodeinituser
45 from piston.models import (
46@@ -880,136 +877,6 @@
47 return mark_safe(get_html_display_for_key(self.key, MAX_KEY_DISPLAY))
48
49
50-# Due for model migration on 2012-05-18
51-class FileStorageManager(Manager):
52- """Manager for `FileStorage` objects.
53-
54- Store files by calling `save_file`. No two `FileStorage` objects can
55- have the same filename at the same time. Writing new data to a file
56- whose name is already in use, replaces its `FileStorage` with one
57- pointing to the new data.
58-
59- Underneath, however, the storage layer will keep the old version of the
60- file around indefinitely. Thus, if the overwriting transaction rolls
61- back, it may leave the new file as garbage on the filesystem; but the
62- original file will not be affected. Also, any ongoing reads from the
63- old file will continue without iterruption.
64- """
65- # The time, in seconds, that an unreferenced file is allowed to
66- # persist in order to satisfy ongoing requests.
67- grace_time = 12 * 60 * 60
68-
69- def get_existing_storage(self, filename):
70- """Return an existing `FileStorage` of this name, or None."""
71- existing_storage = self.filter(filename=filename)
72- if len(existing_storage) == 0:
73- return None
74- elif len(existing_storage) == 1:
75- return existing_storage[0]
76- else:
77- raise AssertionError(
78- "There are %d files called '%s'."
79- % (len(existing_storage), filename))
80-
81- def save_file(self, filename, file_object):
82- """Save the file to the filesystem and persist to the database.
83-
84- The file will end up in MEDIA_ROOT/storage/
85-
86- If a file of that name already existed, it will be replaced by the
87- new contents.
88- """
89- # This probably ought to read in chunks but large files are
90- # not expected. Also note that uploading a file with the same
91- # name as an existing one will cause that file to be written
92- # with a new generated name, and the old one remains where it
93- # is. See https://code.djangoproject.com/ticket/6157 - the
94- # Django devs consider deleting things dangerous ... ha.
95- # HOWEVER - this operation would need to be atomic anyway so
96- # it's safest left how it is for now (reads can overlap with
97- # writes from Juju).
98- content = ContentFile(file_object.read())
99-
100- storage = self.get_existing_storage(filename)
101- if storage is None:
102- storage = FileStorage(filename=filename)
103- storage.data.save(filename, content)
104- return storage
105-
106- def list_stored_files(self):
107- """Find the files stored in the filesystem."""
108- dirs, files = FileStorage.storage.listdir(FileStorage.upload_dir)
109- return [
110- os.path.join(FileStorage.upload_dir, filename)
111- for filename in files]
112-
113- def list_referenced_files(self):
114- """Find the names of files that are referenced from `FileStorage`.
115-
116- :return: All file paths within MEDIA ROOT (relative to MEDIA_ROOT)
117- that have `FileStorage` entries referencing them.
118- :rtype: frozenset
119- """
120- return frozenset(
121- file_storage.data.name
122- for file_storage in self.all())
123-
124- def is_old(self, storage_filename):
125- """Is the named file in the filesystem storage old enough to be dead?
126-
127- :param storage_filename: The name under which the file is stored in
128- the filesystem, relative to MEDIA_ROOT. This need not be the
129- same name as its filename as stored in the `FileStorage` object.
130- It includes the name of the upload directory.
131- """
132- file_path = os.path.join(settings.MEDIA_ROOT, storage_filename)
133- mtime = os.stat(file_path).st_mtime
134- expiry = mtime + self.grace_time
135- return expiry <= time.time()
136-
137- def collect_garbage(self):
138- """Clean up stored files that are no longer accessible."""
139- try:
140- stored_files = self.list_stored_files()
141- except OSError as e:
142- if e.errno != ENOENT:
143- raise
144- logger.info(
145- "Upload directory does not exist yet. "
146- "Skipping garbage collection.")
147- return
148- referenced_files = self.list_referenced_files()
149- for path in stored_files:
150- if path not in referenced_files and self.is_old(path):
151- FileStorage.storage.delete(path)
152-
153-
154-# Due for model migration on 2012-05-18
155-class FileStorage(CleanSave, Model):
156- """A simple file storage keyed on file name.
157-
158- :ivar filename: A unique file name to use for the data being stored.
159- :ivar data: The file's actual data.
160- """
161-
162- class Meta(DefaultMeta):
163- """Needed for South to recognize this model."""
164-
165- storage = FileSystemStorage()
166-
167- upload_dir = "storage"
168-
169- # Unix filenames can be longer than this (e.g. 255 bytes), but leave
170- # some extra room for the full path, as well as a versioning suffix.
171- filename = CharField(max_length=200, unique=True, editable=False)
172- data = FileField(upload_to=upload_dir, storage=storage, max_length=255)
173-
174- objects = FileStorageManager()
175-
176- def __unicode__(self):
177- return self.filename
178-
179-
180 # Register the models in the admin site.
181 admin.site.register(Consumer)
182 admin.site.register(Config)
183
184=== added file 'src/maasserver/models/filestorage.py'
185--- src/maasserver/models/filestorage.py 1970-01-01 00:00:00 +0000
186+++ src/maasserver/models/filestorage.py 2012-05-17 09:20:23 +0000
187@@ -0,0 +1,163 @@
188+# Copyright 2012 Canonical Ltd. This software is licensed under the
189+# GNU Affero General Public License version 3 (see the file LICENSE).
190+
191+"""Storage for uploaded files."""
192+
193+from __future__ import (
194+ absolute_import,
195+ print_function,
196+ unicode_literals,
197+ )
198+
199+__metaclass__ = type
200+__all__ = [
201+ 'FileStorage',
202+ ]
203+
204+
205+from errno import ENOENT
206+import os
207+import time
208+
209+from django.conf import settings
210+from django.core.files.base import ContentFile
211+from django.core.files.storage import FileSystemStorage
212+from django.db.models import (
213+ CharField,
214+ FileField,
215+ Manager,
216+ Model,
217+ )
218+from maasserver import DefaultMeta
219+from maasserver.models.cleansave import CleanSave
220+
221+
222+class FileStorageManager(Manager):
223+ """Manager for `FileStorage` objects.
224+
225+ Store files by calling `save_file`. No two `FileStorage` objects can
226+ have the same filename at the same time. Writing new data to a file
227+ whose name is already in use, replaces its `FileStorage` with one
228+ pointing to the new data.
229+
230+ Underneath, however, the storage layer will keep the old version of the
231+ file around indefinitely. Thus, if the overwriting transaction rolls
232+ back, it may leave the new file as garbage on the filesystem; but the
233+ original file will not be affected. Also, any ongoing reads from the
234+ old file will continue without iterruption.
235+ """
236+ # The time, in seconds, that an unreferenced file is allowed to
237+ # persist in order to satisfy ongoing requests.
238+ grace_time = 12 * 60 * 60
239+
240+ def get_existing_storage(self, filename):
241+ """Return an existing `FileStorage` of this name, or None."""
242+ existing_storage = self.filter(filename=filename)
243+ if len(existing_storage) == 0:
244+ return None
245+ elif len(existing_storage) == 1:
246+ return existing_storage[0]
247+ else:
248+ raise AssertionError(
249+ "There are %d files called '%s'."
250+ % (len(existing_storage), filename))
251+
252+ def save_file(self, filename, file_object):
253+ """Save the file to the filesystem and persist to the database.
254+
255+ The file will end up in MEDIA_ROOT/storage/
256+
257+ If a file of that name already existed, it will be replaced by the
258+ new contents.
259+ """
260+ # This probably ought to read in chunks but large files are
261+ # not expected. Also note that uploading a file with the same
262+ # name as an existing one will cause that file to be written
263+ # with a new generated name, and the old one remains where it
264+ # is. See https://code.djangoproject.com/ticket/6157 - the
265+ # Django devs consider deleting things dangerous ... ha.
266+ # HOWEVER - this operation would need to be atomic anyway so
267+ # it's safest left how it is for now (reads can overlap with
268+ # writes from Juju).
269+ content = ContentFile(file_object.read())
270+
271+ storage = self.get_existing_storage(filename)
272+ if storage is None:
273+ storage = FileStorage(filename=filename)
274+ storage.data.save(filename, content)
275+ return storage
276+
277+ def list_stored_files(self):
278+ """Find the files stored in the filesystem."""
279+ dirs, files = FileStorage.storage.listdir(FileStorage.upload_dir)
280+ return [
281+ os.path.join(FileStorage.upload_dir, filename)
282+ for filename in files]
283+
284+ def list_referenced_files(self):
285+ """Find the names of files that are referenced from `FileStorage`.
286+
287+ :return: All file paths within MEDIA ROOT (relative to MEDIA_ROOT)
288+ that have `FileStorage` entries referencing them.
289+ :rtype: frozenset
290+ """
291+ return frozenset(
292+ file_storage.data.name
293+ for file_storage in self.all())
294+
295+ def is_old(self, storage_filename):
296+ """Is the named file in the filesystem storage old enough to be dead?
297+
298+ :param storage_filename: The name under which the file is stored in
299+ the filesystem, relative to MEDIA_ROOT. This need not be the
300+ same name as its filename as stored in the `FileStorage` object.
301+ It includes the name of the upload directory.
302+ """
303+ file_path = os.path.join(settings.MEDIA_ROOT, storage_filename)
304+ mtime = os.stat(file_path).st_mtime
305+ expiry = mtime + self.grace_time
306+ return expiry <= time.time()
307+
308+ def collect_garbage(self):
309+ """Clean up stored files that are no longer accessible."""
310+ # Avoid circular imports.
311+ from maasserver.models import logger
312+
313+ try:
314+ stored_files = self.list_stored_files()
315+ except OSError as e:
316+ if e.errno != ENOENT:
317+ raise
318+ logger.info(
319+ "Upload directory does not exist yet. "
320+ "Skipping garbage collection.")
321+ return
322+ referenced_files = self.list_referenced_files()
323+ for path in stored_files:
324+ if path not in referenced_files and self.is_old(path):
325+ FileStorage.storage.delete(path)
326+
327+
328+class FileStorage(CleanSave, Model):
329+ """A simple file storage keyed on file name.
330+
331+ :ivar filename: A unique file name to use for the data being stored.
332+ :ivar data: The file's actual data.
333+ """
334+
335+ class Meta(DefaultMeta):
336+ """Needed for South to recognize this model."""
337+
338+ storage = FileSystemStorage()
339+
340+ upload_dir = "storage"
341+
342+ # Unix filenames can be longer than this (e.g. 255 bytes), but leave
343+ # some extra room for the full path, as well as a versioning suffix.
344+ filename = CharField(max_length=200, unique=True, editable=False)
345+ data = FileField(upload_to=upload_dir, storage=storage, max_length=255)
346+
347+ objects = FileStorageManager()
348+
349+ def __unicode__(self):
350+ return self.filename