Example #1
0
class Signature(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                on_delete=models.CASCADE,
                                verbose_name=_("User"))
    signature = models.ImageField(null=True,
                                  blank=True,
                                  upload_to=signature_path,
                                  storage=HashedFilenameStorage())
    timestamp = models.DateTimeField(default=timezone.now)

    class Meta:
        verbose_name = _('Signature')
        verbose_name_plural = _('Signatures')

    def __str__(self):
        return str(self.user)

    def remove_signature_file(self):
        if self.signature and os.path.exists(self.signature.path):
            os.remove(self.signature.path)

    def get_signature_bytes(self):
        if not self.signature:
            return None
        try:
            self.signature.open()
        except IOError:
            # File was deleted, set field to None
            self.signature = None
            self.save()
            return None
        try:
            return self.signature.read()
        finally:
            self.signature.close()
Example #2
0
    def handle(self, *args, **options):
        translation.activate(settings.LANGUAGE_CODE)

        if not hasattr(os, 'scandir'):
            raise NotImplemented('Requires Python 3.5+')

        self.storage = HashedFilenameStorage()

        self.directory = options['directory']
        self.run_all = options.get('run_all') == 'all'
        self.run_now = options.get('run_now') == 'run-now'
        self.missing_attachments = []

        if not self.run_now:
            print('DRY RUN MODE')
        else:
            print('Running for real now')

        for folder in os.scandir(self.directory):
            if folder.is_dir():
                self.handle_folder(folder)

        with open('missing_attachments.json', 'w') as f:
            json.dump(self.missing_attachments, f)
Example #3
0
    def handle(self, *args, **options):
        translation.activate(settings.LANGUAGE_CODE)

        if not hasattr(os, 'scandir'):
            raise NotImplementedError('Requires Python 3.5+')

        self.storage = HashedFilenameStorage()

        self.directory = options['directory']
        self.run_all = options.get('run_all') == 'all'
        self.run_now = options.get('run_now') == 'run-now'
        self.missing_attachments = []

        if not self.run_now:
            print('DRY RUN MODE')
        else:
            print('Running for real now')

        for folder in os.scandir(self.directory):
            if folder.is_dir():
                self.handle_folder(folder)

        with open('missing_attachments.json', 'w') as f:
            json.dump(self.missing_attachments, f)
Example #4
0
class FoiAttachment(models.Model):
    belongs_to = models.ForeignKey(FoiMessage,
                                   null=True,
                                   verbose_name=_("Belongs to message"),
                                   on_delete=models.CASCADE,
                                   related_name='foiattachment_set')
    name = models.CharField(_("Name"), max_length=255)
    file = models.FileField(_("File"),
                            upload_to=upload_to,
                            max_length=255,
                            storage=HashedFilenameStorage(),
                            db_index=True)
    size = models.IntegerField(_("Size"), blank=True, null=True)
    filetype = models.CharField(_("File type"), blank=True, max_length=100)
    format = models.CharField(_("Format"), blank=True, max_length=100)
    can_approve = models.BooleanField(_("User can approve"), default=True)
    approved = models.BooleanField(_("Approved"), default=False)
    redacted = models.ForeignKey('self',
                                 verbose_name=_("Redacted Version"),
                                 null=True,
                                 blank=True,
                                 on_delete=models.SET_NULL,
                                 related_name='unredacted_set')
    is_redacted = models.BooleanField(_("Is redacted"), default=False)
    converted = models.ForeignKey('self',
                                  verbose_name=_("Converted Version"),
                                  null=True,
                                  blank=True,
                                  on_delete=models.SET_NULL,
                                  related_name='original_set')
    is_converted = models.BooleanField(_("Is converted"), default=False)
    timestamp = models.DateTimeField(null=True, default=timezone.now)
    pending = models.BooleanField(default=False)

    document = models.OneToOneField(Document,
                                    null=True,
                                    blank=True,
                                    related_name='attachment',
                                    on_delete=models.SET_NULL)

    objects = FoiAttachmentManager()

    attachment_published = Signal(providing_args=['user'])
    attachment_deleted = Signal(providing_args=['user'])
    attachment_redacted = Signal(providing_args=['user'])
    document_created = Signal(providing_args=['user'])

    class Meta:
        ordering = ('name', )
        unique_together = (("belongs_to", "name"), )
        # order_with_respect_to = 'belongs_to'
        verbose_name = _('Attachment')
        verbose_name_plural = _('Attachments')

    def __str__(self):
        return "%s (%s) of %s" % (self.name, self.size, self.belongs_to)

    def index_content(self):
        return "\n".join((self.name, ))

    def get_html_id(self):
        return _("attachment-%(id)d") % {"id": self.id}

    def get_bytes(self):
        self.file.open(mode='rb')
        try:
            return self.file.read()
        finally:
            self.file.close()

    @property
    def can_redact(self):
        return self.redacted is not None or (self.can_approve and self.is_pdf)

    @property
    def can_delete(self):
        if not self.belongs_to.is_postal:
            return False
        if not self.can_approve:
            return False
        now = timezone.now()
        return self.timestamp > (now - DELETE_TIMEFRAME)

    @property
    def can_edit(self):
        return self.can_redact or self.can_delete or self.can_approve

    @property
    def can_link(self):
        return self.approved or not (self.can_redact and self.can_approve)

    @property
    def is_pdf(self):
        return self.filetype in PDF_FILETYPES or (
            self.name and self.name.endswith(('.pdf', '.PDF'))
            and self.filetype == 'application/octet-stream')

    @property
    def is_word(self):
        return self.filetype in WORD_FILETYPES or (
            self.name and self.name.endswith(WORD_FILEEXTENSIONS))

    @property
    def is_excel(self):
        return self.filetype in EXCEL_FILETYPES or (
            self.name and self.name.endswith(EXCEL_FILEEXTENSIONS))

    @property
    def is_text(self):
        return self.filetype.startswith('text/')

    @property
    def is_archive(self):
        return self.filetype in ARCHIVE_FILETYPES or (
            self.name and self.name.endswith(ARCHIVE_FILEEXTENSIONS))

    @property
    def is_powerpoint(self):
        return self.filetype in POWERPOINT_FILETYPES or (
            self.name and self.name.endswith(POWERPOINT_FILETEXTENSIONS))

    @property
    def is_image(self):
        return (self.filetype.startswith('image/')
                or self.filetype in IMAGE_FILETYPES or self.name.endswith(
                    ('.jpg', '.jpeg', '.gif', '.png')))

    @property
    def is_mail_decoration(self):
        return self.is_image and self.size and self.size < 1024 * 60

    @property
    def is_irrelevant(self):
        return self.is_mail_decoration or self.is_signature

    @property
    def is_signature(self):
        return self.name.endswith(
            ('.p7s', '.vcf', '.asc')) and self.size < 1024 * 15

    @property
    def can_embed(self):
        return self.filetype in EMBEDDABLE_FILETYPES or self.is_pdf

    def get_anchor_url(self):
        if self.belongs_to:
            return self.belongs_to.get_absolute_url()
        return '#' + self.get_html_id()

    def get_domain_anchor_url(self):
        return '%s%s' % (settings.SITE_URL, self.get_anchor_url())

    def get_absolute_url(self):
        fr = self.belongs_to.request
        return reverse('foirequest-show_attachment',
                       kwargs={
                           'slug': fr.slug,
                           'message_id': self.belongs_to.pk,
                           'attachment_name': self.name
                       })

    def get_file_url(self):
        '''
        Hook method for django-filingcabinet
        '''
        return self.get_absolute_domain_file_url()

    def get_file_path(self):
        return self.file.path

    def get_crossdomain_auth(self):
        from ..auth import AttachmentCrossDomainMediaAuth

        return AttachmentCrossDomainMediaAuth({
            'object': self,
        })

    def send_internal_file(self):
        return self.get_crossdomain_auth().send_internal_file()

    def get_absolute_domain_url(self):
        return '%s%s' % (settings.SITE_URL, self.get_absolute_url())

    def get_absolute_domain_auth_url(self):
        return self.get_crossdomain_auth().get_full_auth_url()

    def get_authorized_absolute_domain_file_url(self):
        return self.get_absolute_domain_file_url(authorized=True)

    def get_absolute_domain_file_url(self, authorized=False):
        return self.get_crossdomain_auth().get_full_media_url(
            authorized=authorized)

    def approve_and_save(self):
        self.approved = True
        self.save()
        if self.document:
            foirequest = self.belongs_to.request
            should_be_public = foirequest.public
            if self.document.public != should_be_public:
                self.document.public = should_be_public
                self.document.save()

    def remove_file_and_delete(self):
        if self.file:
            other_references = FoiAttachment.objects.filter(
                file=self.file.name).exclude(id=self.id).exists()
            if not other_references:
                self.file.delete(save=False)
        self.delete()

    def can_convert_to_pdf(self):
        ft = self.filetype.lower()
        name = self.name.lower()
        return (self.converted_id is None
                and can_convert_to_pdf(ft, name=name))

    def create_document(self, title=None):
        if self.document is not None:
            return self.document

        if not self.is_pdf:
            return
        if self.converted_id or self.redacted_id:
            return

        foirequest = self.belongs_to.request
        doc = Document.objects.create(
            original=self,
            user=foirequest.user,
            public=foirequest.public,
            title=title or self.name,
            foirequest=self.belongs_to.request,
            pending=True,
            publicbody=self.belongs_to.sender_public_body)
        self.document = doc
        self.save()
        return doc
Example #5
0
class User(AbstractBaseUser, PermissionsMixin):

    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=
        _('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'
          ),
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(_('email address'),
                              unique=True,
                              null=True,
                              blank=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_(
            'Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    organization = models.CharField(_('Organization'),
                                    blank=True,
                                    max_length=255)
    organization_url = models.URLField(_('Organization URL'),
                                       blank=True,
                                       max_length=255)
    private = models.BooleanField(_('Private'), default=False)
    address = models.TextField(_('Address'), blank=True)
    terms = models.BooleanField(_('Accepted Terms'), default=True)
    newsletter = models.BooleanField(_('Wants Newsletter'), default=False)

    profile_text = models.TextField(blank=True)
    profile_photo = models.ImageField(null=True,
                                      blank=True,
                                      upload_to=profile_photo_path,
                                      storage=HashedFilenameStorage())

    is_trusted = models.BooleanField(_('Trusted'), default=False)
    is_blocked = models.BooleanField(_('Blocked'), default=False)

    is_deleted = models.BooleanField(
        _('deleted'),
        default=False,
        help_text=_('Designates whether this user was deleted.'))
    date_left = models.DateTimeField(_('date left'),
                                     default=None,
                                     null=True,
                                     blank=True)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    def __str__(self):
        if self.email is None:
            return self.username
        return self.email

    def get_absolute_url(self):
        if self.private:
            return ""
        return reverse('account-profile', kwargs={'slug': self.username})

    def get_full_name(self):
        """
        Returns the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        "Returns the short name for the user."
        return self.first_name

    def get_dict(self, fields):
        d = get_dict(self, fields)
        d['request_count'] = self.foirequest_set.all().count()
        return d

    def trusted(self):
        return self.is_trusted or self.is_staff or self.is_superuser

    def show_newsletter(self):
        return has_newsletter() and not self.newsletter

    @classmethod
    def export_csv(cls, queryset):
        fields = (
            "id",
            "first_name",
            "last_name",
            "email",
            "organization",
            "organization_url",
            "private",
            "date_joined",
            "is_staff",
            "address",
            "terms",
            "newsletter",
            "request_count",
        )
        return export_csv(queryset, fields)

    def as_json(self):
        return json.dumps({
            'id': self.id,
            'first_name': self.first_name,
            'last_name': self.last_name,
            'address': self.address,
            'private': self.private,
            'email': self.email,
            'organization': self.organization
        })

    def display_name(self):
        if self.private:
            return str(_("<< Name Not Public >>"))
        else:
            if self.organization:
                return '%s (%s)' % (self.get_full_name(), self.organization)
            else:
                return self.get_full_name()

    def get_account_service(self):
        from .services import AccountService

        return AccountService(self)

    def get_autologin_url(self, url):
        from .services import AccountService

        service = AccountService(self)
        return service.get_autologin_url(url)

    def get_password_change_form(self, *args, **kwargs):
        from .forms import SetPasswordForm
        return SetPasswordForm(self, *args, **kwargs)

    def get_change_form(self, *args, **kwargs):
        from .forms import UserChangeForm
        return UserChangeForm(self, *args, **kwargs)
Example #6
0
class FoiAttachment(models.Model):
    belongs_to = models.ForeignKey(FoiMessage,
                                   null=True,
                                   verbose_name=_("Belongs to request"),
                                   on_delete=models.CASCADE,
                                   related_name='foiattachment_set')
    name = models.CharField(_("Name"), max_length=255)
    file = models.FileField(_("File"),
                            upload_to=upload_to,
                            max_length=255,
                            storage=HashedFilenameStorage(),
                            db_index=True)
    size = models.IntegerField(_("Size"), blank=True, null=True)
    filetype = models.CharField(_("File type"), blank=True, max_length=100)
    format = models.CharField(_("Format"), blank=True, max_length=100)
    can_approve = models.BooleanField(_("User can approve"), default=True)
    approved = models.BooleanField(_("Approved"), default=False)
    redacted = models.ForeignKey('self',
                                 verbose_name=_("Redacted Version"),
                                 null=True,
                                 blank=True,
                                 on_delete=models.SET_NULL,
                                 related_name='unredacted_set')
    is_redacted = models.BooleanField(_("Is redacted"), default=False)
    converted = models.ForeignKey('self',
                                  verbose_name=_("Converted Version"),
                                  null=True,
                                  blank=True,
                                  on_delete=models.SET_NULL,
                                  related_name='original_set')
    is_converted = models.BooleanField(_("Is converted"), default=False)
    timestamp = models.DateTimeField(null=True, default=timezone.now)

    document = models.OneToOneField(Document,
                                    null=True,
                                    blank=True,
                                    related_name='attachment',
                                    on_delete=models.SET_NULL)

    attachment_published = Signal(providing_args=[])

    class Meta:
        ordering = ('name', )
        unique_together = (("belongs_to", "name"), )
        # order_with_respect_to = 'belongs_to'
        verbose_name = _('Attachment')
        verbose_name_plural = _('Attachments')

    def __str__(self):
        return "%s (%s) of %s" % (self.name, self.size, self.belongs_to)

    def index_content(self):
        return "\n".join((self.name, ))

    def get_html_id(self):
        return _("attachment-%(id)d") % {"id": self.id}

    def get_internal_url(self):
        return settings.FOI_MEDIA_URL + self.file.name

    def get_bytes(self):
        self.file.open(mode='rb')
        try:
            return self.file.read()
        finally:
            self.file.close()

    @property
    def can_redact(self):
        return can_redact_file(self.filetype, name=self.name)

    @property
    def can_delete(self):
        if not self.belongs_to.is_postal:
            return False
        if not self.can_approve:
            return False
        now = timezone.now()
        return self.timestamp > (now - DELETE_TIMEFRAME)

    @property
    def pending(self):
        return not self.file

    @property
    def can_edit(self):
        return self.can_redact or self.can_delete or self.can_approve

    @property
    def allow_link(self):
        return self.approved or not (self.can_redact and self.can_approve)

    @property
    def is_pdf(self):
        return self.filetype in PDF_FILETYPES or (
            self.name.endswith('.pdf')
            and self.filetype == 'application/octet-stream')

    @property
    def is_image(self):
        return (self.filetype.startswith('image/')
                or self.filetype in IMAGE_FILETYPES or self.name.endswith(
                    ('.jpg', '.jpeg', '.gif', '.png')))

    @property
    def is_mail_decoration(self):
        return self.is_image and self.size and self.size < 1024 * 60

    @property
    def is_irrelevant(self):
        return self.is_mail_decoration or self.is_signature

    @property
    def is_signature(self):
        return self.name.endswith(
            ('.p7s', '.vcf', '.asc')) and self.size < 1024 * 15

    @property
    def can_embed(self):
        return self.filetype in EMBEDDABLE_FILETYPES or self.is_pdf

    def get_anchor_url(self):
        if self.belongs_to:
            return '%s#%s' % (self.belongs_to.request.get_absolute_url(),
                              self.get_html_id())
        return '#' + self.get_html_id()

    def get_domain_anchor_url(self):
        return '%s%s' % (settings.SITE_URL, self.get_anchor_url())

    def get_absolute_url(self):
        fr = self.belongs_to.request
        return reverse('foirequest-show_attachment',
                       kwargs={
                           'slug': fr.slug,
                           'message_id': self.belongs_to.pk,
                           'attachment_name': self.name
                       })

    def get_absolute_domain_url(self):
        return '%s%s' % (settings.SITE_URL, self.get_absolute_url())

    def get_absolute_file_url(self, authenticated=False):
        if not self.name:
            return ''
        url = reverse('foirequest-auth_message_attachment',
                      kwargs={
                          'message_id': self.belongs_to_id,
                          'attachment_name': self.name
                      })
        if settings.FOI_MEDIA_TOKENS and authenticated:
            signer = TimestampSigner()
            value = signer.sign(url).split(':', 1)[1]
            return '%s?token=%s' % (url, value)
        return url

    def get_file_url(self):
        return self.get_absolute_domain_file_url()

    def get_file_path(self):
        if self.file:
            return self.file.path
        return ''

    def get_authenticated_absolute_domain_file_url(self):
        return self.get_absolute_domain_file_url(authenticated=True)

    def get_absolute_domain_file_url(self, authenticated=False):
        return '%s%s' % (settings.FOI_MEDIA_DOMAIN,
                         self.get_absolute_file_url(
                             authenticated=authenticated))

    def check_token(self, request):
        token = request.GET.get('token')
        if token is None:
            return None
        original = '%s:%s' % (self.get_absolute_file_url(), token)
        signer = TimestampSigner()
        try:
            signer.unsign(original, max_age=settings.FOI_MEDIA_TOKEN_EXPIRY)
        except SignatureExpired:
            return None
        except BadSignature:
            return False
        return True

    def approve_and_save(self):
        self.approved = True
        self.save()
        self.attachment_published.send(sender=self)

    def remove_file_and_delete(self):
        if self.file:
            other_references = FoiAttachment.objects.filter(
                file=self.file.name).exclude(id=self.id).exists()
            if not other_references:
                self.file.delete(save=False)
        self.delete()

    def can_convert_to_pdf(self):
        ft = self.filetype.lower()
        name = self.name.lower()
        return (self.converted_id is None
                and can_convert_to_pdf(ft, name=name))

    def create_document(self, title=None):
        if self.document is not None:
            return self.document

        if not self.is_pdf:
            return

        foirequest = self.belongs_to.request
        doc = Document.objects.create(
            original=self,
            user=foirequest.user,
            public=foirequest.public,
            title=title or self.name,
            foirequest=self.belongs_to.request,
            publicbody=self.belongs_to.sender_public_body)
        self.document = doc
        self.save()
        return doc
Example #7
0
class Command(BaseCommand):
    help = "Moves files to content hash based directory structure"

    def add_arguments(self, parser):
        parser.add_argument('directory', type=str)
        parser.add_argument('run_all', nargs='?', type=str)
        parser.add_argument('run_now', nargs='?', type=str)

    def handle(self, *args, **options):
        translation.activate(settings.LANGUAGE_CODE)

        if not hasattr(os, 'scandir'):
            raise NotImplementedError('Requires Python 3.5+')

        self.storage = HashedFilenameStorage()

        self.directory = options['directory']
        self.run_all = options.get('run_all') == 'all'
        self.run_now = options.get('run_now') == 'run-now'
        self.missing_attachments = []

        if not self.run_now:
            print('DRY RUN MODE')
        else:
            print('Running for real now')

        for folder in os.scandir(self.directory):
            if folder.is_dir():
                self.handle_folder(folder)

        with open('missing_attachments.json', 'w') as f:
            json.dump(self.missing_attachments, f)

    def handle_folder(self, folder):
        for file_entry in os.scandir(folder.path):
            if file_entry.is_file():
                message_id = int(folder.name)
                all_attachments = FoiAttachment.objects.filter(
                    belongs_to_id=message_id
                )
                result = self.handle_file(file_entry, all_attachments)
                if self.run_now and not self.run_all and result:
                    raise Exception('Not running all')
                print('-' * 20)

    def handle_file(self, file_entry, all_attachments):
        orig_file_path = os.path.abspath(file_entry.path)
        print(orig_file_path)

        attachment = self.get_attachment(file_entry.name, all_attachments)
        if attachment is None:
            print('Missing attachments')
            self.missing_attachments.append(orig_file_path)
            return False

        print(attachment.name, attachment.file.name)
        file_path = self.fix_filepath(orig_file_path)
        fixed_attachment_name = os.path.basename(file_path)
        fixed_attachment_name = self.get_unique_name(
            fixed_attachment_name, all_attachments
        )
        attachment.name = fixed_attachment_name

        upload_path = upload_to(attachment, fixed_attachment_name)
        with open(orig_file_path, 'rb') as f:
            stored_path = self.storage._get_content_name(upload_path, f)

        print(fixed_attachment_name, stored_path)

        new_path = os.path.join(settings.MEDIA_ROOT, stored_path)

        print('Move from %s to %s' % (orig_file_path, new_path))
        new_base_path = os.path.dirname(new_path)

        if not os.path.exists(new_path):
            print('Creating dirs for', new_base_path)
            if self.run_now:
                mode_kwarg = {}
                if settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS is not None:
                    mode_kwarg['mode'] = settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS
                os.makedirs(
                    new_base_path,
                    exist_ok=True,
                    **mode_kwarg
                )
                shutil.move(orig_file_path, new_path)
        else:
            print('File already exists!', new_base_path)
        if self.run_now:
            attachment.file.name = stored_path
            attachment.save()
        return True

    def fix_filepath(self, filename):
        match = BROKEN_ISO_NAME.match(filename)
        if match is not None:
            return BROKEN_ISO_NAME.sub('\\1\\2', filename)
        return filename

    def get_attachment(self, name, attachments):
        for att in attachments:
            if att.file.name.endswith(name):
                return att

    def get_unique_name(self, name, attachments):
        name_counter = Counter([a.name for a in attachments])
        index = 0
        while name_counter[name] > 1:
            index += 1
            path, ext = os.path.splitext(name)
            name = '%s_%d%s' % (path, index, ext)
        return name
Example #8
0
class Command(BaseCommand):
    help = "Moves files to content hash based directory structure"

    def add_arguments(self, parser):
        parser.add_argument('directory', type=str)
        parser.add_argument('run_all', nargs='?', type=str)
        parser.add_argument('run_now', nargs='?', type=str)

    def handle(self, *args, **options):
        translation.activate(settings.LANGUAGE_CODE)

        if not hasattr(os, 'scandir'):
            raise NotImplemented('Requires Python 3.5+')

        self.storage = HashedFilenameStorage()

        self.directory = options['directory']
        self.run_all = options.get('run_all') == 'all'
        self.run_now = options.get('run_now') == 'run-now'
        self.missing_attachments = []

        if not self.run_now:
            print('DRY RUN MODE')
        else:
            print('Running for real now')

        for folder in os.scandir(self.directory):
            if folder.is_dir():
                self.handle_folder(folder)

        with open('missing_attachments.json', 'w') as f:
            json.dump(self.missing_attachments, f)

    def handle_folder(self, folder):
        for file_entry in os.scandir(folder.path):
            if file_entry.is_file():
                message_id = int(folder.name)
                all_attachments = FoiAttachment.objects.filter(
                    belongs_to_id=message_id)
                result = self.handle_file(file_entry, all_attachments)
                if self.run_now and not self.run_all and result:
                    raise Exception('Not running all')
                print('-' * 20)

    def handle_file(self, file_entry, all_attachments):
        orig_file_path = os.path.abspath(file_entry.path)
        print(orig_file_path)

        attachment = self.get_attachment(file_entry.name, all_attachments)
        if attachment is None:
            print('Missing attachments')
            self.missing_attachments.append(orig_file_path)
            return False

        print(attachment.name, attachment.file.name)
        file_path = self.fix_filepath(orig_file_path)
        fixed_attachment_name = os.path.basename(file_path)
        fixed_attachment_name = self.get_unique_name(fixed_attachment_name,
                                                     all_attachments)
        attachment.name = fixed_attachment_name

        upload_path = upload_to(attachment, fixed_attachment_name)
        with open(orig_file_path, 'rb') as f:
            stored_path = self.storage._get_content_name(upload_path, f)

        print(fixed_attachment_name, stored_path)

        new_path = os.path.join(settings.MEDIA_ROOT, stored_path)

        print('Move from %s to %s' % (orig_file_path, new_path))
        new_base_path = os.path.dirname(new_path)

        if not os.path.exists(new_path):
            print('Creating dirs for', new_base_path)
            if self.run_now:
                mode_kwarg = {}
                if settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS is not None:
                    mode_kwarg[
                        'mode'] = settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS
                os.makedirs(new_base_path, exist_ok=True, **mode_kwarg)
                shutil.move(orig_file_path, new_path)
        else:
            print('File already exists!', new_base_path)
        if self.run_now:
            attachment.file.name = stored_path
            attachment.save()
        return True

    def fix_filepath(self, filename):
        match = BROKEN_ISO_NAME.match(filename)
        if match is not None:
            return BROKEN_ISO_NAME.sub('\\1\\2', filename)
        return filename

    def get_attachment(self, name, attachments):
        for att in attachments:
            if att.file.name.endswith(name):
                return att

    def get_unique_name(self, name, attachments):
        name_counter = Counter([a.name for a in attachments])
        index = 0
        while name_counter[name] > 1:
            index += 1
            path, ext = os.path.splitext(name)
            name = '%s_%d%s' % (path, index, ext)
        return name