Exemplo n.º 1
0
    def save(self,
             *args,
             current_user=None,
             synchronize=False,
             file_extras=None,
             skip_download=False,
             update_m2m=False,
             update_dataset=True,
             **kwargs):

        if update_m2m:
            return super().save(*args, **kwargs)

        if 'update_fields' in kwargs:
            return super().save(*args, **kwargs)

        # Version précédante de la ressource (avant modification)
        previous, created = self.pk \
            and (Resource.objects.get(pk=self.pk), False) or (None, True)

        if previous:
            # crs est immuable sauf si le jeu de données change (Cf. plus bas)
            self.crs = previous.crs

        # Quelques valeur par défaut à la création de l'instance
        if created:
            self.geo_restriction = False
            self.ogc_services = True
            self.extractable = True
        # La restriction au territoire de compétence désactive toujours les services OGC
        if self.geo_restriction:
            self.ogc_services = False

        self.last_update = timezone.now()

        if created:
            super().save(*args, **kwargs)
            kwargs['force_insert'] = False

        # Quelques contrôles sur les fichiers de données téléversée ou à télécharger
        filename = False
        content_type = None
        file_must_be_deleted = False  # permet d'indiquer si les fichiers doivent être supprimés à la fin de la chaine de traitement
        publish_raw_resource = True  # permet d'indiquer si les ressources brutes sont publiées dans CKAN

        if self.ftp_file and not skip_download:
            filename = self.ftp_file.file.name
            # Si la taille de fichier dépasse la limite autorisée,
            # on traite les données en fonction du type détecté
            if self.ftp_file.size > DATA_TRANSMISSION_SIZE_LIMITATION:
                logger.info("This is a big file: %s." % self.ftp_file.size)

                publish_raw_resource = False  # IMPORTANT

                s0 = str(self.ckan_id)
                s1, s2, s3 = s0[:3], s0[3:6], s0[6:]
                dir = os.path.join(CKAN_STORAGE_PATH, s1, s2)
                os.makedirs(dir, mode=0o777, exist_ok=True)

                logger.info("cp %s %s" % (filename, os.path.join(dir, s3)))
                shutil.copyfile(filename, os.path.join(dir, s3))

                src = os.path.join(dir, s3)
                dst = os.path.join(dir, filename.split('/')[-1])
                logger.info("ln -s %s %s" % (dst, src))
                try:
                    os.symlink(src, dst)
                except (FileNotFoundError, FileExistsError) as e:
                    logger.exception(e)
                    logger.warning("Error was ignored.")
                    pass

        elif (self.up_file and file_extras):
            # GDAL/OGR ne semble pas prendre de fichier en mémoire..
            # ..à vérifier mais si c'est possible comment indiquer le vsi en préfixe du filename ?
            filename = self.up_file.path
            self.save(update_fields=('up_file', ))
            file_must_be_deleted = True

        elif self.dl_url and not skip_download:
            try:
                directory, filename, content_type = download(
                    self.dl_url,
                    settings.MEDIA_ROOT,
                    max_size=DOWNLOAD_SIZE_LIMIT)
            except SizeLimitExceededError as e:
                logger.exception(e)
                l = len(str(e.max_size))
                if l > 6:
                    m = '{0} mo'.format(Decimal(int(e.max_size) / 1024 / 1024))
                elif l > 3:
                    m = '{0} ko'.format(Decimal(int(e.max_size) / 1024))
                else:
                    m = '{0} octets'.format(int(e.max_size))
                raise ValidationError(('La taille du fichier dépasse '
                                       'la limite autorisée : {0}.').format(m),
                                      code='dl_url')
            except Exception as e:
                logger.exception(e)
                if e.__class__.__name__ == 'HTTPError':
                    if e.response.status_code == 404:
                        msg = ('La ressource distante ne semble pas exister. '
                               "Assurez-vous que l'URL soit correcte.")
                    if e.response.status_code == 403:
                        msg = ("Vous n'avez pas l'autorisation pour "
                               'accéder à la ressource.')
                    if e.response.status_code == 401:
                        msg = ('Une authentification est nécessaire '
                               'pour accéder à la ressource.')
                else:
                    msg = 'Le téléchargement du fichier a échoué.'
                raise ValidationError(msg, code='dl_url')
            file_must_be_deleted = True

        # Synchronisation avec CKAN
        # =========================

        # La synchronisation doit s'effectuer avant la publication des
        # éventuelles couches de données SIG car dans le cas des données
        # de type « raster », nous utilisons le filestore de CKAN.
        if synchronize:
            if publish_raw_resource:
                self.synchronize(content_type=content_type,
                                 file_extras=file_extras,
                                 filename=filename,
                                 with_user=current_user)
            else:
                url = reduce(urljoin, [
                    CKAN_URL, 'dataset/',
                    str(self.dataset.ckan_id) + '/', 'resource/',
                    str(self.ckan_id) + '/', 'download/',
                    Path(self.ftp_file.name).name
                ])
                self.synchronize(url=url, with_user=current_user)

        # Détection des données SIG
        # =========================
        if filename:
            # On vérifie s'il s'agit de données SIG, uniquement pour
            # les extensions de fichier autorisées..
            extension = self.format_type.extension.lower()
            if self.format_type.is_gis_format:
                # Si c'est le cas, on monte les données dans la base PostGIS dédiée
                # et on déclare la couche au service OGC:WxS de l'organisation.

                # Mais d'abord, on vérifie si la ressource contient
                # déjà des « Layers », auquel cas il faudra vérifier si
                # la table de données a changée.
                existing_layers = {}
                if not created:
                    existing_layers = dict(
                        (re.sub('^(\w+)_[a-z0-9]{7}$', '\g<1>', layer.name),
                         layer.name) for layer in self.get_layers())

                try:

                    # C'est carrément moche mais c'est pour aller vite.
                    # Il faudrait factoriser tout ce bazar et créer
                    # un décorateur pour gérer le rool-back sur CKAN.

                    try:
                        gdalogr_obj = get_gdalogr_object(filename, extension)
                    except NotDataGISError:
                        tables = []
                        pass
                    else:

                        try:
                            self.format_type = ResourceFormats.objects.get(
                                extension=extension,
                                ckan_format=gdalogr_obj.format)
                        # except ResourceFormats.MultipleObjectsReturned:
                        #     pass
                        except Exception as e:
                            logger.exception(e)
                            logger.warning("Error was ignored.")
                            pass

                        # ==========================
                        # Jeu de données vectorielle
                        # ==========================

                        if gdalogr_obj.__class__.__name__ == 'OgrOpener':

                            # On convertit les données vers PostGIS

                            try:
                                tables = ogr2postgis(
                                    gdalogr_obj,
                                    update=existing_layers,
                                    epsg=self.crs and self.crs.auth_code
                                    or None,
                                    encoding=self.encoding)

                            except NotOGRError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    "Le fichier reçu n'est pas reconnu "
                                    'comme étant un jeu de données SIG correct.'
                                )
                                raise ValidationError(msg, code='__all__')

                            except DataDecodingError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Impossible de décoder correctement les '
                                    "données. Merci d'indiquer l'encodage "
                                    'ci-dessous.')
                                raise ValidationError(msg, code='encoding')

                            except WrongDataError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource contient des données SIG que '
                                    'nous ne parvenons pas à lire correctement. '
                                    'Un ou plusieurs objets sont erronés.')
                                raise ValidationError(msg)

                            except NotFoundSrsError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais nous ne parvenons pas à détecter le système '
                                    'de coordonnées. Merci de sélectionner le code du '
                                    'CRS dans la liste ci-dessous.')
                                raise ValidationError(msg, code='crs')

                            except NotSupportedSrsError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais le système de coordonnées de celles-ci '
                                    "n'est pas supporté par l'application.")
                                raise ValidationError(msg, code='__all__')

                            except ExceedsMaximumLayerNumberFixedError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                raise ValidationError(e.__str__(),
                                                      code='__all__')

                            else:
                                # Ensuite, pour tous les jeux de données SIG trouvés,
                                # on crée le service ows à travers la création de `Layer`
                                try:
                                    Layer = apps.get_model(
                                        app_label='idgo_admin',
                                        model_name='Layer')
                                    for table in tables:
                                        try:
                                            Layer.objects.get(name=table['id'],
                                                              resource=self)
                                        except Layer.DoesNotExist:
                                            save_opts = {
                                                'synchronize': synchronize
                                            }
                                            bbox = transform(
                                                table['bbox'], table['epsg'])
                                            Layer.vector.create(
                                                bbox=bbox,
                                                name=table['id'],
                                                resource=self,
                                                save_opts=save_opts)
                                except Exception as e:
                                    logger.exception(e)
                                    file_must_be_deleted and remove_file(
                                        filename)
                                    for table in tables:
                                        drop_table(table['id'])
                                    raise e

                        # ==========================
                        # Jeu de données matricielle
                        # ==========================

                        if gdalogr_obj.__class__.__name__ == 'GdalOpener':

                            coverage = gdalogr_obj.get_coverage()

                            try:
                                tables = [
                                    gdalinfo(
                                        coverage,
                                        update=existing_layers,
                                        epsg=self.crs and self.crs.auth_code
                                        or None)
                                ]

                            except NotFoundSrsError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais nous ne parvenons pas à détecter le système '
                                    'de coordonnées. Merci de sélectionner le code du '
                                    'CRS dans la liste ci-dessous.')
                                raise ValidationError(msg, code='crs')

                            except NotSupportedSrsError as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                msg = (
                                    'Votre ressource semble contenir des données SIG '
                                    'mais le système de coordonnées de celles-ci '
                                    "n'est pas supporté par l'application.")
                                raise ValidationError(msg, code='__all__')

                            # Super Crado Code
                            s0 = str(self.ckan_id)
                            s1, s2, s3 = s0[:3], s0[3:6], s0[6:]
                            dir = os.path.join(CKAN_STORAGE_PATH, s1, s2)
                            src = os.path.join(dir, s3)
                            dst = os.path.join(dir, filename.split('/')[-1])
                            try:
                                os.symlink(src, dst)
                            except FileExistsError as e:
                                logger.exception(e)
                            except FileNotFoundError as e:
                                logger.exception(e)
                            else:
                                logger.debug(
                                    'Created a symbolic link {dst} pointing to {src}.'
                                    .format(dst=dst, src=src))

                            try:
                                Layer = apps.get_model(app_label='idgo_admin',
                                                       model_name='Layer')
                                for table in tables:
                                    try:
                                        Layer.objects.get(name=table['id'],
                                                          resource=self)
                                    except Layer.DoesNotExist:
                                        Layer.raster.create(bbox=table['bbox'],
                                                            name=table['id'],
                                                            resource=self)
                            except Exception as e:
                                logger.exception(e)
                                file_must_be_deleted and remove_file(filename)
                                raise e

                except Exception as e:
                    logger.exception(e)
                    if created:
                        if current_user:
                            username = current_user.username
                            apikey = CkanHandler.get_user(username)['apikey']
                            with CkanUserHandler(apikey) as ckan:
                                ckan.delete_resource(str(self.ckan_id))
                        else:
                            CkanHandler.delete_resource(str(self.ckan_id))
                        for layer in self.get_layers():
                            layer.delete(current_user=current_user)
                    # Puis on « raise » l'erreur
                    raise e

                # On met à jour les champs de la ressource
                SupportedCrs = apps.get_model(app_label='idgo_admin',
                                              model_name='SupportedCrs')
                crs = [
                    SupportedCrs.objects.get(auth_name='EPSG',
                                             auth_code=table['epsg'])
                    for table in tables
                ]
                # On prend la première valeur (c'est moche)
                self.crs = crs and crs[0] or None

                # Si les données changent..
                if existing_layers and \
                        set(previous.get_layers()) != set(self.get_layers()):
                    # on supprime les anciens `layers`..
                    for layer in previous.get_layers():
                        layer.delete()
        ####
        if self.get_layers():
            extent = self.get_layers().aggregate(
                models.Extent('bbox')).get('bbox__extent')
            if extent:
                xmin, ymin = extent[0], extent[1]
                xmax, ymax = extent[2], extent[3]
                setattr(self, 'bbox', bounds_to_wkt(xmin, ymin, xmax, ymax))
        else:
            # Si la ressource n'est pas de type SIG, on passe les trois arguments
            # qui concernent exclusivement ces dernières à « False ».
            self.geo_restriction = False
            self.ogc_services = False
            self.extractable = False

        super().save(*args, **kwargs)

        # Puis dans tous les cas..
        # on met à jour le statut des couches du service cartographique..
        if not created:
            self.update_enable_layers_status()

        # on supprime les données téléversées ou téléchargées..
        if file_must_be_deleted:
            remove_file(filename)

        # [Crado] on met à jour la ressource CKAN
        if synchronize:
            try:
                CkanHandler.update_resource(str(self.ckan_id),
                                            extracting_service=str(
                                                self.extractable))
            except Exception as e:
                logger.exception(e)
                logger.warning("Error was ignored.")

        for layer in self.get_layers():
            layer.save(synchronize=synchronize)

        if update_dataset:
            self.dataset.date_modification = timezone.now().date()
            self.dataset.save(current_user=None,
                              synchronize=True,
                              update_fields=['date_modification'])
Exemplo n.º 2
0
    def synchronize(self, url=None, filename=None, content_type=None,
                    file_extras=None, with_user=None):
        """Synchronizer le jeu de données avec l'instance de CKAN."""
        # Identifiant de la resource CKAN :
        id = str(self.ckan_id)

        # Définition des propriétés du « package » :
        data = {
            'crs': self.crs and self.crs.description or '',
            'name': self.title,
            'description': self.description,
            'data_type': self.data_type,
            'extracting_service': 'False',  # I <3 CKAN
            'format': self.format_type and self.format_type.ckan_format,
            'view_type': self.format_type and self.format_type.ckan_view,
            'id': id,
            'lang': self.lang,
            'restricted_by_jurisdiction': str(self.geo_restriction),
            'url': url and url or '',
            'api': '{}'}

        # TODO: Factoriser

        # (0) Aucune restriction
        if self.restricted_level == 'public':
            restricted = json.dumps({'level': 'public'})
        # (1) Uniquement pour un utilisateur connecté
        elif self.restricted_level == 'registered':
            restricted = json.dumps({'level': 'registered'})
        # (2) Seulement les utilisateurs indiquées
        elif self.restricted_level == 'only_allowed_users':
            restricted = json.dumps({
                'allowed_users': ','.join(
                    self.profiles_allowed.exists() and [
                        p.user.username for p
                        in self.profiles_allowed.all()] or []),
                'level': 'only_allowed_users'})
        # (3) Les utilisateurs de cette organisation
        elif self.restricted_level == 'same_organization':
            restricted = json.dumps({
                'allowed_users': ','.join(
                    get_all_users_for_organisations(
                        self.organisations_allowed.all())),
                'level': 'only_allowed_users'})
        # (3) Les utilisateurs des organisations indiquées
        elif self.restricted_level == 'any_organization':
            restricted = json.dumps({
                'allowed_users': ','.join(
                    get_all_users_for_organisations(
                        self.organisations_allowed.all())),
                'level': 'only_allowed_users'})

        data['restricted'] = restricted

        if self.referenced_url:
            data['url'] = self.referenced_url

        if self.dl_url and filename:
            downloaded_file = File(open(filename, 'rb'))
            data['upload'] = downloaded_file
            data['size'] = downloaded_file.size
            data['mimetype'] = content_type

        if self.up_file and file_extras:
            data['upload'] = self.up_file.file
            data['size'] = file_extras.get('size')
            data['mimetype'] = file_extras.get('mimetype')

        if self.ftp_file:
            if not url:
                data['upload'] = self.ftp_file.file
            data['size'] = self.ftp_file.size
            if self.format_type and len(self.format_type.mimetype):
                data['mimetype'] = self.format_type.mimetype[0]
            else:
                data['mimetype'] = 'text/plain'
            # Passe dans un validateur pour forcer en base : url_type='upload'
            data['force_url_type'] = 'upload'

        if self.data_type == 'raw':
            if self.ftp_file or self.dl_url or self.up_file:
                data['resource_type'] = 'file.upload'
            elif self.referenced_url:
                data['resource_type'] = 'file'
        if self.data_type == 'annexe':
            data['resource_type'] = 'documentation'
        if self.data_type == 'service':
            data['resource_type'] = 'api'

        ckan_package = CkanHandler.get_package(str(self.dataset.ckan_id))

        if with_user:
            username = with_user.username

            apikey = CkanHandler.get_user(username)['apikey']
            with CkanUserHandler(apikey=apikey) as ckan:
                ckan.publish_resource(ckan_package, **data)
        else:
            return CkanHandler.publish_resource(ckan_package, **data)