thumbor-libs-blackhand 0.1.6__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. thumbor_libs_blackhand/loaders/__init__.py +2 -4
  2. thumbor_libs_blackhand/loaders/mongodb_loader.py +2 -4
  3. thumbor_libs_blackhand/loaders/spec_http_or_specb_loader.py +2 -3
  4. thumbor_libs_blackhand/loaders/specb_file_fallback_file_loader.py +2 -4
  5. thumbor_libs_blackhand/metrics/__init__.py +2 -4
  6. thumbor_libs_blackhand/mongodb/pool_result_storage.py +60 -0
  7. thumbor_libs_blackhand/result_storages/__init__.py +4 -0
  8. thumbor_libs_blackhand/result_storages/hybrid_result_storage.py +213 -0
  9. thumbor_libs_blackhand/result_storages/mongodb_result_storage_v3.py +196 -0
  10. thumbor_libs_blackhand/result_storages/mongodb_result_storage_v4.py +209 -0
  11. thumbor_libs_blackhand/result_storages/mongodb_result_storage_v5.py +178 -0
  12. thumbor_libs_blackhand/storages/__init__.py +4 -0
  13. thumbor_libs_blackhand/storages/mongodb_webp_storage.py +5 -12
  14. thumbor_libs_blackhand/url_signers/__init__.py +2 -4
  15. thumbor_libs_blackhand/url_signers/base64_hmac_sha1_trim.py +3 -3
  16. thumbor_libs_blackhand-0.5.0.dist-info/LICENSE +21 -0
  17. {thumbor_libs_blackhand-0.1.6.dist-info → thumbor_libs_blackhand-0.5.0.dist-info}/METADATA +7 -4
  18. thumbor_libs_blackhand-0.5.0.dist-info/RECORD +20 -0
  19. {thumbor_libs_blackhand-0.1.6.dist-info → thumbor_libs_blackhand-0.5.0.dist-info}/WHEEL +1 -1
  20. thumbor_libs_blackhand/result_storages/mongodb_webp_result_storage.py +0 -117
  21. thumbor_libs_blackhand-0.1.6.dist-info/LICENSE +0 -674
  22. thumbor_libs_blackhand-0.1.6.dist-info/RECORD +0 -16
  23. {thumbor_libs_blackhand-0.1.6.dist-info → thumbor_libs_blackhand-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
1
- #!/usr/bin/python
2
1
  # -*- coding: utf-8 -*-
3
-
4
2
  # Blackhand library for Thumbor
5
- # Licensed under the GNU/GPL license:
6
- # https://fsf.org/
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
@@ -1,11 +1,9 @@
1
- #!/usr/bin/python
2
1
  # -*- coding: utf-8 -*-
3
2
  # Blackhand library for Thumbor
4
- # Licensed under the GNU/GPL license:
5
- # https://fsf.org/
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
6
5
 
7
6
  from pymongo.mongo_client import MongoClient
8
- from pymongo.server_api import ServerApi
9
7
  from bson.objectid import ObjectId
10
8
  from thumbor.loaders import LoaderResult
11
9
  import gridfs
@@ -1,8 +1,7 @@
1
- #!/usr/bin/python
2
1
  # -*- coding: utf-8 -*-
3
2
  # Blackhand library for Thumbor
4
- # Licensed under the GNU/GPL license:
5
- # https://fsf.org/
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
6
5
 
7
6
  from thumbor.loaders import http_loader
8
7
  from thumbor_libs_blackhand.loaders import specb_file_fallback_file_loader
@@ -1,8 +1,7 @@
1
- #!/usr/bin/python
2
1
  # -*- coding: utf-8 -*-
3
2
  # Blackhand library for Thumbor
4
- # Licensed under the GNU/GPL license:
5
- # https://fsf.org/
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
6
5
 
7
6
  from os import fstat
8
7
  from datetime import datetime
@@ -30,7 +29,6 @@ async def load(context, path):
30
29
  result.error = LoaderResult.ERROR_NOT_FOUND
31
30
  result.successful = False
32
31
  return result
33
- pass
34
32
 
35
33
  if not exists(file_path):
36
34
  file_path = unquote(file_path)
@@ -1,6 +1,4 @@
1
- #!/usr/bin/python
2
1
  # -*- coding: utf-8 -*-
3
-
4
2
  # Blackhand library for Thumbor
5
- # Licensed under the GNU/GPL license:
6
- # https://fsf.org/
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
@@ -0,0 +1,60 @@
1
+ # Licensed under the MIT license:
2
+ # http://www.opensource.org/licenses/mit-license
3
+ # Copyright (c) 2020 Eka Cahya Pratama <ekapratama93@gmail.com>
4
+
5
+ from motor.motor_tornado import MotorClient
6
+ from pymongo import ASCENDING, DESCENDING
7
+ from tornado.gen import convert_yielded
8
+
9
+
10
+ class Singleton(type):
11
+ """
12
+ Define an Instance operation that lets clients access its unique
13
+ instance.
14
+ """
15
+
16
+ def __init__(cls, name, bases, attrs, **kwargs):
17
+ super().__init__(name, bases, attrs)
18
+ cls._instance = None
19
+
20
+ def __call__(cls, *args, **kwargs):
21
+ if cls._instance is None:
22
+ cls._instance = super().__call__(*args, **kwargs)
23
+ return cls._instance
24
+
25
+
26
+ class MongoConnector(metaclass=Singleton):
27
+
28
+ def __init__(self,
29
+ uri=None,
30
+ host=None,
31
+ port=None,
32
+ db_name=None,
33
+ col_name=None):
34
+ self.uri = uri
35
+ self.host = host
36
+ self.port = port
37
+ self.db_name = db_name
38
+ self.col_name = col_name
39
+ self.db_conn, self.col_conn = self.create_connection()
40
+ convert_yielded(self.ensure_index())
41
+
42
+ def create_connection(self):
43
+ if self.uri:
44
+ connection = MotorClient(self.uri)
45
+ else:
46
+ connection = MotorClient(self.host, self.port)
47
+
48
+ db_conn = connection[self.db_name]
49
+ col_conn = db_conn[self.col_name]
50
+
51
+ return db_conn, col_conn
52
+
53
+ async def ensure_index(self):
54
+ index_name = 'key_1_created_at_-1'
55
+ indexes = await self.col_conn.index_information()
56
+ if index_name not in indexes:
57
+ await self.col_conn.create_index(
58
+ [('path', ASCENDING), ('created_at', DESCENDING)],
59
+ name=index_name
60
+ )
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Blackhand library for Thumbor
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
@@ -0,0 +1,213 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Blackhand library for Thumbor
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
5
+ # libvips
6
+
7
+ from urllib import parse #, request; error
8
+ #from motor.motor_tornado import MotorGridFSBucket
9
+ #from pymongo.errors import PyMongoError
10
+ from thumbor_libs_blackhand.mongodb.pool_result_storage import MongoConnector
11
+ from datetime import datetime, timedelta
12
+ from thumbor.result_storages import BaseStorage, ResultStorageResult
13
+ from thumbor.engines import BaseEngine
14
+ from thumbor.utils import logger
15
+ from bson.binary import Binary
16
+ from uuid import uuid4
17
+ import pytz
18
+ import re
19
+ #import os
20
+
21
+
22
+ class Storage(BaseStorage):
23
+
24
+ def __init__(self, context):
25
+ BaseStorage.__init__(self, context)
26
+ self.database, self.storage = self.__conn__()
27
+ super(Storage, self).__init__(context)
28
+
29
+
30
+ def __conn__(self):
31
+ '''Return the MongoDB params.
32
+ :returns: MongoDB DB and Collection
33
+ :rtype: pymongo.database.Database, pymongo.database.Collection
34
+ '''
35
+
36
+ password = parse.quote_plus(self.context.config.MONGO_RESULT_STORAGE_SERVER_PASSWORD)
37
+ user = parse.quote_plus(self.context.config.MONGO_RESULT_STORAGE_SERVER_USER)
38
+
39
+ if not self.context.config.MONGO_RESULT_STORAGE_SERVER_REPLICASET:
40
+ uric = ('mongodb://'+ user +':' + password + '@' + self.context.config.MONGO_RESULT_STORAGE_SERVER_HOSTS
41
+ + '/?authSource=' + self.context.config.MONGO_RESULT_STORAGE_SERVER_DB)
42
+ else:
43
+ uric = ('mongodb://'+ user +':' + password + '@' + self.context.config.MONGO_RESULT_STORAGE_SERVER_HOSTS
44
+ + '/?authSource=' + self.context.config.MONGO_RESULT_STORAGE_SERVER_DB
45
+ + "&replicaSet=" + self.context.config.MONGO_RESULT_STORAGE_SERVER_REPLICASET
46
+ + "&readPreference=" + self.context.config.MONGO_RESULT_STORAGE_SERVER_READ_PREFERENCE)
47
+
48
+ db_name = self.context.config.MONGO_RESULT_STORAGE_SERVER_DB
49
+ col_name = self.context.config.MONGO_RESULT_STORAGE_SERVER_COLLECTION
50
+
51
+ try:
52
+ uri = self.context.config.MONGO_RESULT_STORAGE_URI
53
+ except AttributeError:
54
+ uri=uric
55
+
56
+ try:
57
+ host = self.context.config.MONGO_RESULT_STORAGE_SERVER_HOST
58
+ port = self.context.config.MONGO_RESULT_STORAGE_SERVER_PORT
59
+ except AttributeError:
60
+ host=None
61
+ port=None
62
+
63
+ mongo_conn = MongoConnector(
64
+ db_name=db_name,
65
+ col_name=col_name,
66
+ uri=uri,
67
+ host=host,
68
+ port=port,
69
+ )
70
+
71
+ database = mongo_conn.db_conn
72
+ storage = mongo_conn.col_conn
73
+
74
+ return database, storage
75
+
76
+
77
+ @property
78
+ def is_auto_webp(self):
79
+ return self.context.config.AUTO_WEBP and self.context.request.accepts_webp
80
+
81
+ @property
82
+ def is_auto_webp(self):
83
+ '''Return if webp.
84
+ :return: If the file is a webp
85
+ :rettype: boolean
86
+ '''
87
+
88
+ return self.context.config.AUTO_WEBP \
89
+ and self.context.request.accepts_webp
90
+
91
+ def get_key_from_request(self):
92
+ '''Return a path key for the current request url.
93
+ :return: The storage key for the current url
94
+ :rettype: string
95
+ '''
96
+
97
+ path = f"result:{self.context.request.url}"
98
+
99
+ if self.is_auto_webp:
100
+ return f'{path}/webp'
101
+
102
+ return path
103
+
104
+
105
+ def get_max_age(self):
106
+
107
+ return self.context.config.RESULT_STORAGE_EXPIRATION_SECONDS
108
+
109
+ async def put(self, image_bytes):
110
+ key = self.get_key_from_request()
111
+ #max_age = self.get_max_age()
112
+ #result_ttl = self.get_max_age()
113
+ try:
114
+ CACHE_PATH = self.context.config.CACHE_PATH
115
+ except AttributeError:
116
+ raise
117
+ imgpath = ( datetime.now().strftime('%Y') + '/'
118
+ + datetime.now().strftime('%m') + '/' + datetime.now().strftime('%d')
119
+ + '/' + datetime.now().strftime('%H'))
120
+ mkpath = (CACHE_PATH + '/' + imgpath)
121
+ self.ensure_dir(mkpath)
122
+
123
+ #os.makedirs(mkpath)
124
+ ref_img = ''
125
+ ref_img = re.findall(r'/[a-zA-Z0-9]{24}(?:$|/)', key)
126
+ if ref_img:
127
+ ref_img2 = ref_img[0].replace('/','')
128
+ else:
129
+ ref_img2 = 'undef'
130
+
131
+
132
+ if self.context.config.get("MONGO_STORE_METADATA", False):
133
+ metadata = dict(self.context.headers)
134
+ else:
135
+ metadata = {}
136
+
137
+ cache_id = str(uuid4())
138
+ endingpath= mkpath + "/" + cache_id
139
+ doc = {
140
+ 'path': key,
141
+ 'created_at': datetime.utcnow(),
142
+ 'data': "",
143
+ 'metadata': metadata,
144
+ 'content_type': BaseEngine.get_mimetype(image_bytes),
145
+ 'ref_id': ref_img2,
146
+ 'content_length' : len(image_bytes),
147
+ 'cache_path': imgpath,
148
+ 'cache_id' : cache_id
149
+ }
150
+ doc_cpm = dict(doc)
151
+
152
+
153
+ fichier = open(endingpath, "wb")
154
+ try:
155
+ fichier.write(Binary(image_bytes))
156
+ finally:
157
+ fichier.close()
158
+
159
+ await self.storage.insert_one(doc_cpm)
160
+ #return self.context.request.url
161
+ return key
162
+
163
+
164
+ async def get(self):
165
+
166
+ key = self.get_key_from_request()
167
+ logger.debug("[RESULT_STORAGE] image not found at %s", key)
168
+ try:
169
+ CACHE_PATH = self.context.config.CACHE_PATH
170
+ except AttributeError:
171
+ raise
172
+ age = datetime.utcnow() - timedelta(
173
+ seconds=self.get_max_age()
174
+ )
175
+ stored = await self.storage.find_one({
176
+ 'path': key,
177
+ 'created_at': {
178
+ '$gte': age
179
+ },
180
+ }, {
181
+ 'ref_id': True,
182
+ 'created_at': True,
183
+ 'metadata': True,
184
+ 'content_type': True,
185
+ 'data' : True,
186
+ 'content_length': True,
187
+ 'cache_path' : True,
188
+ 'cache_id' : True,
189
+ })
190
+
191
+
192
+ if not stored:
193
+ return None
194
+ metadata = stored['metadata']
195
+ metadata['LastModified'] = stored['created_at'].replace(
196
+ tzinfo=pytz.utc
197
+ )
198
+ metadata['Cache-Control'] = "max-age=60,public"
199
+ metadata['ContentLength'] = stored['content_length']
200
+ metadata['ContentType'] = stored['content_type']
201
+ cachefile = CACHE_PATH + '/' + stored['cache_path'] + "/" + stored['cache_id']
202
+
203
+ fichier = open(cachefile, "rb")
204
+ try:
205
+ tosend = fichier.read()
206
+ finally:
207
+ fichier.close()
208
+
209
+ return ResultStorageResult(
210
+ buffer=tosend,
211
+ metadata=metadata,
212
+ successful=True
213
+ )
@@ -0,0 +1,196 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Blackhand library for Thumbor
3
+ # Licensed under the MIT license:
4
+ # http://www.opensource.org/licenses/mit-license
5
+
6
+ from urllib import parse #, request; error
7
+ #from motor.motor_tornado import MotorGridFSBucket
8
+ #from pymongo.errors import PyMongoError
9
+ from thumbor_libs_blackhand.mongodb.pool_result_storage import MongoConnector
10
+ from datetime import datetime, timedelta
11
+ from thumbor.result_storages import BaseStorage, ResultStorageResult
12
+ from thumbor.engines import BaseEngine
13
+ from thumbor.utils import logger
14
+ from bson.binary import Binary
15
+ import pytz
16
+ import re
17
+
18
+
19
+ class Storage(BaseStorage):
20
+
21
+ def __init__(self, context):
22
+ BaseStorage.__init__(self, context)
23
+ self.database, self.storage = self.__conn__()
24
+ super(Storage, self).__init__(context)
25
+
26
+
27
+
28
+ def __conn__(self):
29
+ '''Return the MongoDB params.
30
+ :returns: MongoDB DB and Collection
31
+ :rtype: pymongo.database.Database, pymongo.database.Collection
32
+ '''
33
+
34
+ password = parse.quote_plus(self.context.config.MONGO_RESULT_STORAGE_SERVER_PASSWORD)
35
+ user = parse.quote_plus(self.context.config.MONGO_RESULT_STORAGE_SERVER_USER)
36
+ if not self.context.config.MONGO_RESULT_STORAGE_SERVER_REPLICASET:
37
+ uric = ('mongodb://'+ user +':' + password + '@' + self.context.config.MONGO_RESULT_STORAGE_SERVER_HOSTS
38
+ + '/?authSource=' + self.context.config.MONGO_RESULT_STORAGE_SERVER_DB)
39
+ else:
40
+ uric = ('mongodb://'+ user +':' + password + '@' + self.context.config.MONGO_RESULT_STORAGE_SERVER_HOSTS
41
+ + '/?authSource=' + self.context.config.MONGO_RESULT_STORAGE_SERVER_DB
42
+ + "&replicaSet=" + self.context.config.MONGO_RESULT_STORAGE_SERVER_REPLICASET
43
+ + "&readPreference=" + self.context.config.MONGO_RESULT_STORAGE_SERVER_READ_PREFERENCE)
44
+
45
+ db_name = self.context.config.MONGO_RESULT_STORAGE_SERVER_DB
46
+ col_name = self.context.config.MONGO_RESULT_STORAGE_SERVER_COLLECTION
47
+ host = None
48
+ port = None
49
+
50
+ try:
51
+ uri = self.context.config.MONGO_RESULT_STORAGE_URI
52
+ except AttributeError:
53
+ uri=uric
54
+
55
+ try:
56
+ host = self.context.config.MONGO_RESULT_STORAGE_SERVER_HOST
57
+ port = self.context.config.MONGO_RESULT_STORAGE_SERVER_PORT
58
+ except AttributeError:
59
+ host=None
60
+ port=None
61
+
62
+ mongo_conn = MongoConnector(
63
+ db_name=db_name,
64
+ col_name=col_name,
65
+ uri=uri,
66
+ host=host,
67
+ port=port,
68
+ )
69
+
70
+ database = mongo_conn.db_conn
71
+ storage = mongo_conn.col_conn
72
+
73
+ return database, storage
74
+
75
+ @property
76
+ def is_auto_webp(self):
77
+ '''Return if webp.
78
+ :return: If the file is a webp
79
+ :rettype: boolean
80
+ '''
81
+
82
+ return self.context.config.AUTO_WEBP \
83
+ and self.context.request.accepts_webp
84
+
85
+ def get_key_from_request(self):
86
+ '''Return a path key for the current request url.
87
+ :return: The storage key for the current url
88
+ :rettype: string
89
+ '''
90
+
91
+ path = f"result:{self.context.request.url}"
92
+
93
+ if self.is_auto_webp:
94
+ return f'{path}/webp'
95
+
96
+ return path
97
+
98
+
99
+ def get_max_age(self):
100
+
101
+ return self.context.config.RESULT_STORAGE_EXPIRATION_SECONDS
102
+
103
+
104
+ async def put(self, image_bytes):
105
+ key = self.get_key_from_request()
106
+ #max_age = self.get_max_age()
107
+ #result_ttl = self.get_max_age()
108
+ ref_img = ''
109
+ ref_img = re.findall(r'/[a-zA-Z0-9]{24}(?:$|/)', key)
110
+ if ref_img:
111
+ ref_img2 = ref_img[0].replace('/','')
112
+ else:
113
+ ref_img2 = 'undef'
114
+
115
+
116
+ if self.context.config.get("MONGO_STORE_METADATA", False):
117
+ metadata = dict(self.context.headers)
118
+ else:
119
+ metadata = {}
120
+
121
+ doc = {
122
+ 'path': key,
123
+ 'created_at': datetime.utcnow(),
124
+ 'data': Binary(image_bytes),
125
+ 'metadata': metadata,
126
+ 'content_type': BaseEngine.get_mimetype(image_bytes),
127
+ 'ref_id': ref_img2,
128
+ 'content_length' : len(image_bytes)
129
+ }
130
+ doc_cpm = dict(doc)
131
+
132
+ await self.storage.insert_one(doc_cpm)
133
+ #return self.context.request.url
134
+ return key
135
+
136
+ async def get(self):
137
+ key = self.get_key_from_request()
138
+ logger.debug("[RESULT_STORAGE] image not found at %s", key)
139
+
140
+
141
+ age = datetime.utcnow() - timedelta(
142
+ seconds=self.get_max_age()
143
+ )
144
+ stored = await self.storage.find_one({
145
+ 'path': key,
146
+ 'created_at': {
147
+ '$gte': age
148
+ },
149
+ }, {
150
+ 'ref_id': True,
151
+ 'created_at': True,
152
+ 'metadata': True,
153
+ 'content_type': True,
154
+ 'data' : True,
155
+ 'content_length': True,
156
+ })
157
+
158
+
159
+ if not stored:
160
+ return None
161
+
162
+ try:
163
+ #self.context.config.MONGO_RESULT_STORAGE_MAXCACHESIZE
164
+ dedup = self.context.config.MONGO_RESULT_STORAGE_DEDUP
165
+ except:
166
+ dedup = False
167
+
168
+ if not dedup:
169
+ logger.debug("Deduplication OFF")
170
+ else:
171
+ filter={
172
+ 'path': key
173
+ }
174
+ sort=list({
175
+ 'created_at': -1
176
+ }.items())
177
+ skip=1
178
+ obj = self.storage.find(filter=filter, skip=skip)
179
+ async for doc in obj:
180
+ logger.info("Deduplication %s", key)
181
+ self.storage.delete_one({"_id": doc["_id"]})
182
+
183
+
184
+ metadata = stored['metadata']
185
+ metadata['LastModified'] = stored['created_at'].replace(
186
+ tzinfo=pytz.utc
187
+ )
188
+ metadata['Cache-Control'] = "max-age=60,public"
189
+ metadata['ContentLength'] = stored['content_length']
190
+ metadata['ContentType'] = stored['content_type']
191
+
192
+ return ResultStorageResult(
193
+ buffer=stored['data'],
194
+ metadata=metadata,
195
+ successful=True
196
+ )