]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/nopaste.py
wiki: Only match usernames when a word starts with @
[ipfire.org.git] / src / backend / nopaste.py
1 #!/usr/bin/python3
2
3 import asyncio
4 import datetime
5 import io
6 import ipaddress
7 import logging
8 import magic
9 import struct
10 import tornado.iostream
11 import tornado.tcpserver
12
13 from . import base
14 from .misc import Object
15 from .decorators import *
16
17 # Setup logging
18 log = logging.getLogger(__name__)
19
20 CHUNK_SIZE = 1024 ** 2
21
22 class Nopaste(Object):
23 def _get_paste(self, query, *args, **kwargs):
24 return self.db.fetch_one(Paste, query, *args, **kwargs)
25
26 def create(self, content, account, subject=None, mimetype=None, expires=None, address=None):
27 # Convert any text to bytes
28 if isinstance(content, str):
29 content = content.encode("utf-8")
30
31 # Store the blob
32 blob_id = self._store_blob(content)
33
34 # Guess the mimetype if none set
35 if not mimetype:
36 mimetype = magic.from_buffer(content, mime=True)
37
38 if expires:
39 expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires)
40
41 # http://blog.00null.net/easily-generating-random-strings-in-postgresql/
42 paste = self._get_paste("""
43 INSERT INTO
44 nopaste
45 (
46 uuid,
47 account,
48 subject,
49 expires_at,
50 address,
51 mimetype,
52 size,
53 blob_id
54 )
55 VALUES
56 (
57 random_slug(), %s, %s, %s, %s, %s, %s, %s
58 )
59 RETURNING
60 *
61 """, account.uid, subject, expires or None, address, mimetype, len(content), blob_id,
62 )
63
64 # Log result
65 log.info("Created a new paste (%s) of %s byte(s) from %s (%s - %s)" % (
66 paste.uuid, paste.size, paste.address, paste.asn or "N/A", paste.country or "N/A",
67 ))
68
69 return paste
70
71 def _fetch_blob(self, id):
72 blob = self.db.get("""
73 SELECT
74 data
75 FROM
76 nopaste_blobs
77 WHERE
78 id = %s
79 """, id,
80 )
81
82 if blob:
83 return blob.data
84
85 def _store_blob(self, data):
86 """
87 Stores the blob by sending it to the database and returning its ID
88 """
89 blob = self.db.get("""
90 INSERT INTO
91 nopaste_blobs
92 (
93 data
94 )
95 VALUES
96 (
97 %s
98 )
99 ON CONFLICT
100 (
101 digest(data, 'sha256')
102 )
103 DO UPDATE SET
104 last_uploaded_at = CURRENT_TIMESTAMP
105 RETURNING
106 id
107 """, data,
108 )
109
110 # Return the ID
111 return blob.id
112
113 def get(self, uuid):
114 paste = self._get_paste("""
115 SELECT
116 *
117 FROM
118 nopaste
119 WHERE
120 uuid = %s
121 AND (
122 expires_at >= CURRENT_TIMESTAMP
123 OR
124 expires_at IS NULL
125 )
126 """, uuid,
127 )
128
129 return paste
130
131 def cleanup(self):
132 """
133 Removes all expired pastes and removes any unneeded blobs
134 """
135 # Remove all expired pastes
136 self.db.execute("""
137 DELETE FROM
138 nopaste
139 WHERE
140 expires_at < CURRENT_TIMESTAMP
141 """)
142
143 # Remove unneeded blobs
144 self.db.execute("""
145 DELETE FROM
146 nopaste_blobs
147 WHERE NOT EXISTS
148 (
149 SELECT
150 1
151 FROM
152 nopaste
153 WHERE
154 nopaste.blob_id = nopaste_blobs.id
155 )
156 """)
157
158
159 class Paste(Object):
160 def init(self, id, data):
161 self.id, self.data = id, data
162
163 def __str__(self):
164 return self.subject or self.uuid
165
166 # UUID
167
168 @property
169 def uuid(self):
170 return self.data.uuid
171
172 # Subject
173
174 @property
175 def subject(self):
176 return self.data.subject
177
178 # Created At
179
180 @property
181 def created_at(self):
182 return self.data.created_at
183
184 time_created = created_at
185
186 # Expires At
187
188 @property
189 def expires_at(self):
190 return self.data.expires_at
191
192 time_expires = expires_at
193
194 # Account
195
196 @lazy_property
197 def account(self):
198 return self.backend.accounts.get_by_uid(self.data.account)
199
200 # Blob
201
202 @lazy_property
203 def blob(self):
204 return self.backend.nopaste._fetch_blob(self.data.blob_id)
205
206 content = blob
207
208 # Size
209
210 @property
211 def size(self):
212 return self.data.size
213
214 # MIME Type
215
216 @property
217 def mimetype(self):
218 return self.data.mimetype or "application/octet-stream"
219
220 # Address
221
222 @property
223 def address(self):
224 return self.data.address
225
226 # Location
227
228 @lazy_property
229 def location(self):
230 return self.backend.location.lookup("%s" % self.address)
231
232 # ASN
233
234 @lazy_property
235 def asn(self):
236 if self.location and self.location.asn:
237 return self.backend.location.get_as(self.location.asn)
238
239 # Country
240
241 @lazy_property
242 def country(self):
243 if self.location and self.location.country_code:
244 return self.backend.location.get_country(self.location.country_code)
245
246 # Viewed?
247
248 def viewed(self):
249 """
250 Call this when this paste has been viewed/downloaded/etc.
251 """
252 self.db.execute("""
253 UPDATE
254 nopaste
255 SET
256 last_accessed_at = CURRENT_TIMESTAMP,
257 views = views + 1
258 WHERE
259 id = %s
260 """, self.id,
261 )