]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/nopaste.py
nopaste: Tidy up the cleanup task
[ipfire.org.git] / src / backend / nopaste.py
CommitLineData
8b82926a
MT
1#!/usr/bin/python3
2
539d08da 3import asyncio
8b82926a 4import datetime
539d08da
MT
5import io
6import logging
e3bc8f21 7import magic
539d08da
MT
8import tornado.iostream
9import tornado.tcpserver
66862195 10
539d08da 11from . import base
11347e46 12from .misc import Object
672150b2 13from .decorators import *
66862195 14
539d08da
MT
15# Setup logging
16log = logging.getLogger(__name__)
17
18CHUNK_SIZE = 1024 ** 2
19
66862195 20class Nopaste(Object):
672150b2
MT
21 def _get_paste(self, query, *args, **kwargs):
22 return self.db.fetch_one(Paste, query, *args, **kwargs)
23
e3bc8f21 24 def create(self, content, subject=None, mimetype=None, expires=None, account=None, address=None):
2268f20b
MT
25 # Store the blob
26 blob_id = self._store_blob(content)
27
e3bc8f21
MT
28 # Guess the mimetype if none set
29 if not mimetype:
30 mimetype = magic.from_buffer(content, mime=True)
31
66862195
MT
32 uid = None
33 if account:
34 uid = account.uid
35
8b82926a
MT
36 if expires:
37 expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires)
38
66862195 39 # http://blog.00null.net/easily-generating-random-strings-in-postgresql/
672150b2 40 paste = self._get_paste("""
2268f20b
MT
41 INSERT INTO
42 nopaste
43 (
44 uuid,
45 subject,
46 content,
47 time_expires,
48 address,
49 uid,
50 mimetype,
51 size,
52 blob_id
53 )
54 VALUES
55 (
56 random_slug(), %s, %s, %s, %s, %s, %s, %s, %s
57 )
58 RETURNING
672150b2 59 *
2268f20b
MT
60 """, subject, content, expires or None, address, uid, mimetype, len(content), blob_id,
61 )
66862195 62
672150b2
MT
63 # Log result
64 log.info("Created a new paste (%s) of %s byte(s) from %s (%s - %s)" % (
65 paste.uuid, paste.size, paste.address, paste.asn or "N/A", paste.country or "N/A",
66 ))
67
68 return paste
69
70 def _fetch_blob(self, id):
71 blob = self.db.get("""
72 SELECT
73 data
74 FROM
75 nopaste_blobs
76 WHERE
77 id = %s
78 """, id,
79 )
80
81 if blob:
82 return blob.data
66862195 83
2268f20b
MT
84 def _store_blob(self, data):
85 """
86 Stores the blob by sending it to the database and returning its ID
87 """
88 blob = self.db.get("""
89 INSERT INTO
90 nopaste_blobs
91 (
92 data
93 )
94 VALUES
95 (
96 %s
97 )
98 ON CONFLICT
99 (
100 data
101 )
102 DO UPDATE SET
103 last_uploaded_at = CURRENT_TIMESTAMP
104 RETURNING
105 id
106 """, data
107 )
108
109 # Return the ID
110 return blob.id
111
66862195 112 def get(self, uuid):
672150b2
MT
113 paste = self._get_paste("""
114 SELECT
115 *
116 FROM
117 nopaste
118 WHERE
119 uuid = %s
120 AND (
121 expires_at >= CURRENT_TIMESTAMP
122 OR
123 expires_at IS NULL
124 )
125 """, uuid,
126 )
66862195 127
672150b2 128 return paste
66862195 129
0de07bc3 130 def get_content(self, uuid):
09fa25a8
MT
131 res = self.db.get("SELECT content FROM nopaste \
132 WHERE uuid = %s", uuid)
0de07bc3 133
09fa25a8 134 if res:
a41085fb 135 return bytes(res.content)
0de07bc3 136
66862195 137 def _update_lastseen(self, uuid):
bfbf061b 138 self.db.execute("UPDATE nopaste SET time_lastseen = NOW(), views = views + 1 \
66862195
MT
139 WHERE uuid = %s", uuid)
140
063fd092
MT
141 def cleanup(self):
142 """
143 Removes all expired pastes and removes any unneeded blobs
144 """
145 # Remove all expired pastes
146 self.db.execute("""
147 DELETE FROM
148 nopaste
149 WHERE
150 expires_at < CURRENT_TIMESTAMP
151 """)
152
153 # Remove unneeded blobs
154 self.db.execute("""
155 DELETE FROM
156 nopaste_blobs
157 WHERE NOT EXISTS
158 (
159 SELECT
160 1
161 FROM
162 nopaste
163 WHERE
164 nopaste.blob_id = nopaste_blobs.id
165 )
166 """)
539d08da
MT
167
168
169class Paste(Object):
170 def init(self, id, data):
171 self.id, self.data = id, data
172
173 # UUID
174
175 @property
176 def uuid(self):
177 return self.data.uuid
178
179 # Subject
180
181 @property
182 def subject(self):
183 return self.data.subject
184
185 # Created At
186
187 @property
188 def created_at(self):
189 return self.data.created_at
190
191 time_created = created_at
192
193 # Expires At
194
195 @property
196 def expires_at(self):
197 return self.data.expires_at
198
199 time_expires = expires_at
200
201 # Blob
202
203 @lazy_property
204 def blob(self):
205 return self.backend.nopaste._fetch_blob(self.data.blob_id)
206
207 content = blob
208
209 # Size
210
211 @property
212 def size(self):
213 return self.data.size
214
672150b2
MT
215 # MIME Type
216
217 @property
218 def mimetype(self):
219 return self.data.mimetype or "application/octet-stream"
220
539d08da
MT
221 # Address
222
223 @property
224 def address(self):
225 return self.data.address
226
227 # Location
228
229 @lazy_property
230 def location(self):
231 return self.backend.location.lookup("%s" % self.address)
232
233 # ASN
234
235 @lazy_property
236 def asn(self):
237 if self.location and self.location.asn:
238 return self.backend.location.get_asn(self.location.asn)
239
240 # Country
241
242 @lazy_property
243 def country(self):
244 if self.location and self.location.country_code:
245 return self.backend.location.get_country(self.location.country_code)
246
672150b2
MT
247 # Legacy
248
249 @property
250 def account(self):
251 return None
252
539d08da
MT
253
254class Service(tornado.tcpserver.TCPServer):
255 def __init__(self, config, **kwargs):
256 # Initialize backend
257 self.backend = base.Backend(config)
258
259 super().__init__(**kwargs)
260
261 async def handle_stream(self, stream, address):
262 buffer = io.BytesIO()
263
264 # Read the entire stream
265 try:
266 while True:
267 chunk = await stream.read_bytes(CHUNK_SIZE, partial=True)
268
269 log.debug("Read a chunk of %s byte(s)" % len(chunk))
270
271 # Write the chunk into the buffer
272 buffer.write(chunk)
273
274 # If we have read less then we have reached the end
275 if len(chunk) < CHUNK_SIZE:
276 break
277
278 # End if the stream closed unexpectedly
279 except tornado.iostream.StreamClosedError as e:
280 return
281
282 log.debug("Finished reading payload")
283
284 # Process address
285 address, port = address
286
287 # Store this into the database
288 with self.backend.db.transaction():
289 uuid = self.backend.nopaste.create(
290 buffer.getvalue(),
291 subject="Streamed Upload",
292 address=address,
293 )
294
295 # Format a response message
296 message = "https://nopaste.ipfire.org/view/%s\n" % uuid
297
298 # Send the message
299 await stream.write(message.encode("utf-8"))
300
301 # We are done, close the stream
302 stream.close()