Exemple #1
0
    def analyze(self, task):
        """Analyze an asset"""
        asset = task.get_asset(
            projection={
                'variations': {'$sub.': Variation}
            }
        )

        file = task.get_file()

        history = []
        for analyzer in task.get_analyzers(asset):
            analyzer.analyze(self.config, asset, file, history)
            history.append(analyzer)

        if task.notification_url:

            # POST the result to the notification URL
            account = Account.by_id(
                asset.account,
                projection={'api_key': True}
            )

            task.post_notification(
                account.api_key,
                json.dumps(asset.to_json_type())
            )

        return {}
Exemple #2
0
def trim_api_logs():
    """
    Ensure the API logs for all accounts do not contain entries that exceed the
    maximum retention period.

    CRON: This task should be run daily during less active periods.
    """

    trim_after = datetime.utcnow() \
            - current_app.config['API_LOG_RETENTION_PERIOD']

    for account_id in Account.ids():

        keys = [f'log:succeeded:{account_id}', f'log:failed:{account_id}']

        for key in keys:
            log_entries = current_app.redis.lrange(key, 0, -1)
            log_entries = [json.loads(e) for e in log_entries]

            for i, log_entry in enumerate(log_entries):
                called = datetime.utcfromtimestamp(log_entry['called'])

                if called <= trim_after:
                    current_app.redis.ltrim(key, 0, i - 1)
                    break
Exemple #3
0
    def generate_variation(self, task):
        """Generate variation for the asset"""

        asset = task.get_asset(
            projection={
                'variations': {
                    '$sub.': Variation
                }
            }
        )

        file = task.get_file()
        native_file = None

        history = []
        for transform in task.get_transforms(asset):
            native_file = transform.transform(
                self.config,
                asset,
                file,
                task.variation_name,
                native_file,
                history
            )
            history.append(transform)

        if task.notification_url:

            # POST the result to the notification URL
            account = Account.by_id(
                asset.account,
                projection={'api_key': True}
            )

            task.post_notification(
                account.api_key,
                json.dumps(asset.to_json_type())
            )

        return {}
Exemple #4
0
    def get_backend(self, secure=False):
        """
        Return the storage backend for the account. Raise an error if the
        request backend isn't configured.
        """

        backend_type = 'secure' if secure else 'public'

        account = Account.by_id(
            self.account._id,
            projection={f'{backend_type}_backend_settings': True}
        )

        backend = getattr(account, f'{backend_type}_backend', None)

        if not backend:
            raise APIError(
                'invalid_request',
                hint=f'No backend configured for {backend_type} storage.'
            )

        return backend
Exemple #5
0
    def get_file(self):
        """Get the file for the asset the task will be run against"""

        account = Account.by_id(self.account_id,
                                projection={
                                    'public_backend_settings': True,
                                    'secure_backend_settings': True
                                })

        asset = self.get_asset(projection={
            'secure': True,
            'name': True,
            'uid': True,
            'ext': True
        })

        if asset.secure:
            backend = account.secure_backend
        else:
            backend = account.public_backend

        assert backend, 'No backend configured for the asset'

        return backend.retrieve(asset.store_key)
Exemple #6
0
    def _store_variation(
        self,
        config,
        asset,
        variation_name,
        versioned,
        ext,
        meta,
        file
    ):
        """
        Store a new variation of the asset. This method both stores the
        variation (and removes any existing variation with the same name) as
        well as updating the asset's `variations` field with details of the
        new variation.
        """

        # Get the account and backend associated with the asset
        account = Account.by_id(
            asset.account,
            projection={
                'public_backend_settings': True,
                'secure_backend_settings': True
            }
        )

        if asset.secure:
            backend = account.secure_backend
        else:
            backend = account.public_backend

        assert backend, 'No backend configured for the asset'

        # Ensure the asset's variation value is a dictionary (in case it's
        # never been set before).
        if not asset.variations:
            asset.variations = {}

        # Create the new variation
        old_variation = asset.variations.get(variation_name)

        new_variation = Variation(
            content_type=mimetypes.guess_type(f'f.{ext}')[0] if ext else '',
            ext=ext,
            meta=meta,
            version=(
                Variation.next_version(
                    old_variation.version if old_variation else None
                )
                if versioned else None
            )
        )

        # Store the new variation
        new_store_key = new_variation.get_store_key(asset, variation_name)
        file.seek(0)
        backend.store(file, new_store_key)

        # Add the new variation's details to the asset's `variations` field
        asset.variations[variation_name] = new_variation
        asset.modified = datetime.utcnow()

        # Apply the updated to the database
        Asset.get_collection().update(
            (Q._id == asset._id).to_dict(),
            {
                '$set': {
                    f'variations.{variation_name}': \
                            new_variation.to_json_type(),
                    'modified': asset.modified
                }
            }
        )

        # Remove the existing variation
        if old_variation:
            old_store_key = old_variation.get_store_key(asset, variation_name)
            if new_store_key != old_store_key:
                backend.delete(old_store_key)

        # Update the asset stats
        new_length = new_variation.meta['length']
        old_length = 0
        if old_variation:
            old_length = old_variation.meta['length']

        Stats.inc(
            account,
            today_tz(tz=config['TIMEZONE']),
            {
                'variations': 0 if old_variation else 1,
                'length': new_length - old_length
            }
        )
Exemple #7
0
    async def prepare(self):

        from blueprints.accounts.models import Account, Stats

        # (Re)Set the account attribute
        self._account = None

        # (Re)Set the write log
        self._write_log = ''

        # (Re)Set the call timer
        self._call_timer = time.time()

        # Check a valid API key has been provided
        api_key = self.request.headers.get('X-H51-APIKey')

        if not api_key:
            raise APIError('unauthorized', 'No authorization key provided.')

        # Find the calling account
        account = Account.one(
            Q.api_key == api_key,
            projection={
                'api_allowed_ip_addresses': True,
                'api_rate_limit_per_second': True
            }
        )

        # Store a reference to the account document against the request handler
        self._account = account

        if not account:
            raise APIError('unauthorized', 'API key not recognized.')

        if account.api_allowed_ip_addresses:

            # Check the caller's IP address is allowed
            ip_address = self.request.headers.get('X-Real-Ip', '')
            if ip_address not in account.api_allowed_ip_addresses:
                raise APIError(
                    'forbidden',
                    (
                        f'The IP address {ip_address} is not allowed to call '
                        'the API for this account.'
                    )
                )

        # Record this request
        rate_key = account.get_rate_key()
        ttl = await self.redis.pttl(rate_key)

        if ttl > 0:
            await self.redis.incr(rate_key)

        else:
            multi = self.redis.multi_exec()
            multi.incr(rate_key)
            multi.expire(rate_key, 1)
            await multi.execute()

        # Apply rate limit
        request_count = int((await self.redis.get(rate_key)) or 0)
        rate_limit = account.api_rate_limit_per_second \
                or self.config['API_RATE_LIMIT_PER_SECOND']

        if request_count > rate_limit:
            raise APIError('request_limit_exceeded')

        # Set the remaining requests allowed this second in the response
        # headers.
        rate_key_reset = max(0, ttl / 1000.0)
        rate_key_reset += time.time()

        self.set_header('X-H51-RateLimit-Limit', str(rate_limit))
        self.set_header(
            'X-H51-RateLimit-Remaining',
            str(rate_limit - request_count)
        )
        self.set_header('X-H51-RateLimit-Reset', str(rate_key_reset))

        # Update the API call stats
        Stats.inc(
            account,
            today_tz(tz=self.config['TIMEZONE']),
            {'api_calls': 1}
        )