Commits

Jannis Leidel  committed 47dc7fc

Added vcstorage app

  • Participants
  • Parent commits 4ee53d1

Comments (0)

Files changed (3)

File src/vcstorage/__init__.py

Empty file added.

File src/vcstorage/fields.py

+import os
+from django.core.files.base import ContentFile
+from django.db.models import signals, TextField, FileField
+
+from vcstorage.storage import VcStorage
+
+DEFAULT_UPLOAD_TO = '%(app_label)s/%(model_name)s/%(field_name)s'
+
+class VcFileField(FileField):
+    """
+    A file field that uses the VcStorage for version control
+    """
+    def __init__(self, upload_to='', storage=None, **kwargs):
+        if storage is None:
+            storage = VcStorage()
+        elif not isinstance(storage, VcStorage):
+            raise TypeError("'storage' is not an instance of %s." % VcStorage)
+        if not upload_to:
+            upload_to = DEFAULT_UPLOAD_TO
+        super(VcFileField, self).__init__(
+            upload_to=upload_to, storage=storage, **kwargs)
+
+    def pre_save(self, model_instance, add):
+        "Returns field's value just before saving."
+        file = super(VcFileField, self).pre_save(model_instance, add)
+        if model_instance.pk:
+            old_name = getattr(model_instance.__class__._default_manager.get(
+                               pk=model_instance.pk), self.name)
+            if file and file.name != old_name and \
+                not model_instance.__class__._default_manager.filter(
+                    **{self.name: old_name}).exclude(pk=model_instance.pk):
+                        self.storage.delete(old_name)
+        return file
+
+    def generate_filename(self, instance, filename):
+        format_kwargs = {
+            'app_label': instance._meta.app_label,
+            'model_name': instance._meta.object_name.lower(),
+            'instance_pk': instance.pk,
+            'field_name': self.attname,
+        }
+        self.upload_to = self.upload_to % format_kwargs
+        return super(VcFileField, self).generate_filename(instance, filename)
+
+class VcTextField(TextField):
+    """
+    """
+    def __init__(self, *args, **kwargs):
+        """
+        Allow specifying a different format for the key used to identify
+        versionized content in the model-definition.
+
+        """
+        self.storage = kwargs.pop('storage', VcStorage())
+        self.key_format = kwargs.pop('key_format',
+            os.path.join(DEFAULT_UPLOAD_TO, '%(instance_pk)s.txt'))
+        # so we can figure out that this field is versionized
+        super(VcTextField, self).__init__(self, *args, **kwargs)
+
+    def get_internal_type(self):
+        return "TextField"
+
+    def post_save(self, instance=None, **kwargs):
+        data = getattr(instance, self.attname).encode('utf-8')
+        format_kwargs = get_format_kwargs(instance, self.attname)
+        key = self.key_format % format_kwargs
+        self.storage.save(key, ContentFile(data))
+
+    def post_delete(self, instance=None, **kwargs):
+        format_kwargs = get_format_kwargs(instance, self.attname)
+        key = self.key_format % format_kwargs
+        self.storage.delete(key)
+
+    def contribute_to_class(self, cls, name):
+        super(VcTextField, self).contribute_to_class(cls, name)
+        signals.post_save.connect(self.post_save, sender=cls)
+        signals.post_delete.connect(self.post_delete, sender=cls)

File src/vcstorage/storage.py

+import os
+import urlparse
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.files.storage import FileSystemStorage
+from django.utils.encoding import smart_str
+
+DEFAULT_BACKEND = getattr(settings, 'VCSTORAGE_DEFAULT_BACKEND', None)
+SUPPORTED_BACKENDS = ('mercurial', 'bazaar', 'git')
+
+class VcStorage(FileSystemStorage):
+    """
+    A Django Storage class that can use anyvc's backends, e.g. 'hg'
+    """
+    backend = None
+    repo = None
+    wd = None
+
+    def __init__(self, location=None, base_url=None):
+        if location is None:
+            location = settings.MEDIA_ROOT
+        if base_url is None:
+            base_url = settings.MEDIA_URL
+        if self.backend is None:
+            if DEFAULT_BACKEND is None:
+                raise ImproperlyConfigured(
+                    "You must define VCSTORAGE_DEFAULT_BACKEND setting or "
+                    "pass a backend parameter to the storage instance.")
+            self.backend = DEFAULT_BACKEND.lower()
+        self.location = os.path.join(os.path.realpath(location), self.backend)
+        self.base_url = urlparse.urljoin(base_url, self.backend.lower())
+        if not self.base_url.endswith("/"):
+            self.base_url = self.base_url+"/"
+
+    def load_working_dir(self, retry=False):
+        """
+        Gets a working dir manager from anyvc
+        """
+        if self.wd:
+            return
+        from anyvc.workdir import get_workdir_manager_for_path
+        wd = get_workdir_manager_for_path(self.location)
+        if wd is None:
+            self.create_repository()
+            wd = get_workdir_manager_for_path(self.location)
+            if wd is None:
+                raise ImproperlyConfigured(
+                    "You must define VCSTORAGE_DEFAULT_BACKEND setting or "
+                    "pass a backend parameter to the storage instance.")
+        self.wd = wd
+
+    def load_repository(self):
+        """
+        Loads the repository class from anyvc
+        """
+        from anyvc import repository, metadata
+        lookup = {}
+        for k, v in repository.lookup.items():
+            if k.lower() in SUPPORTED_BACKENDS:
+                lookup[k.lower()] = v
+        if self.backend in metadata.aliases:
+            self.backend = metadata.aliases[self.backend]
+        return lookup.get(self.backend, None)
+
+    def create_repository(self):
+        """
+        Loads/creates the repository only if required.
+        """
+        if self.repo:
+            return
+        repo_cls = self.load_repository()
+        if repo_cls is None:
+            raise ImproperlyConfigured(
+                "Backend '%s' could not be loaded. Either it couldn't be "
+                "found or it's not supported." % self.backend)
+        self.repo = repo_cls(path=self.location, create=True)
+
+    def save(self, name, content, message=None):
+        """ 
+        Saves the given content with the name and commits to the working dir.
+        """
+        self.load_working_dir()
+        if message is None:
+            message = "Automated commit: adding %s" % name
+        name = super(VcStorage, self).save(name, content)
+        full_paths = [smart_str(os.path.join(self.location, self.path(name)))]
+        try:
+            self.wd.add(paths=full_paths)
+            self.wd.commit(message=message, paths=full_paths)
+        except OSError:
+            pass
+        return name
+
+    def delete(self, name, message=None):
+        """
+        Deletes the specified file from the storage system.
+        """
+        self.load_working_dir()
+        if message is None:
+            message = "Automated commit: removing %s" % name
+        full_paths = [smart_str(self.path(name))]
+        try:
+            self.wd.remove(paths=full_paths)
+            self.wd.commit(message=message, paths=full_paths)
+        except OSError:
+            pass
+
+class GitStorage(VcStorage):
+    """
+    A storage class that will use the Git backend of anyvc
+    """
+    backend = 'git'
+
+class MercurialStorage(VcStorage):
+    """
+    A storage class that will use the Mercurial backend of anyvc
+    """
+    backend = 'hg'
+
+class BazaarStorage(VcStorage):
+    """
+    A storage class that will use the Bazaar backend of anyvc
+    """
+    backend = 'bzr'