#!/usr/bin/python3
+import asyncio
import datetime
+import io
+import ipaddress
+import logging
+import magic
+import struct
+import tornado.iostream
+import tornado.tcpserver
+from . import base
from .misc import Object
+from .decorators import *
+
+# Setup logging
+log = logging.getLogger(__name__)
+
+CHUNK_SIZE = 1024 ** 2
class Nopaste(Object):
- def create(self, subject, content, mimetype="text", expires=None, account=None, address=None):
- self._cleanup_database()
+ def _get_paste(self, query, *args, **kwargs):
+ return self.db.fetch_one(Paste, query, *args, **kwargs)
+
+ def create(self, content, account, subject=None, mimetype=None, expires=None, address=None):
+ # Convert any text to bytes
+ if isinstance(content, str):
+ content = content.encode("utf-8")
- uid = None
- if account:
- uid = account.uid
+ # Store the blob
+ blob_id = self._store_blob(content)
+
+ # Guess the mimetype if none set
+ if not mimetype:
+ mimetype = magic.from_buffer(content, mime=True)
if expires:
expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=expires)
# http://blog.00null.net/easily-generating-random-strings-in-postgresql/
- res = self.db.get("INSERT INTO nopaste(uuid, subject, content, time_expires, address, \
- uid, mimetype, size) VALUES(random_slug(), %s, %s, %s, %s, %s, %s, %s) RETURNING uuid",
- subject, content, expires, address, uid, mimetype, len(content))
+ paste = self._get_paste("""
+ INSERT INTO
+ nopaste
+ (
+ uuid,
+ account,
+ subject,
+ expires_at,
+ address,
+ mimetype,
+ size,
+ blob_id
+ )
+ VALUES
+ (
+ random_slug(), %s, %s, %s, %s, %s, %s, %s
+ )
+ RETURNING
+ *
+ """, account.uid, subject, expires or None, address, mimetype, len(content), blob_id,
+ )
+
+ # Log result
+ log.info("Created a new paste (%s) of %s byte(s) from %s (%s - %s)" % (
+ paste.uuid, paste.size, paste.address, paste.asn or "N/A", paste.country or "N/A",
+ ))
+
+ return paste
- if res:
- return res.uuid
+ def _fetch_blob(self, id):
+ blob = self.db.get("""
+ SELECT
+ data
+ FROM
+ nopaste_blobs
+ WHERE
+ id = %s
+ """, id,
+ )
+
+ if blob:
+ return blob.data
+
+ def _store_blob(self, data):
+ """
+ Stores the blob by sending it to the database and returning its ID
+ """
+ blob = self.db.get("""
+ INSERT INTO
+ nopaste_blobs
+ (
+ data
+ )
+ VALUES
+ (
+ %s
+ )
+ ON CONFLICT
+ (
+ digest(data, 'sha256')
+ )
+ DO UPDATE SET
+ last_uploaded_at = CURRENT_TIMESTAMP
+ RETURNING
+ id
+ """, data,
+ )
+
+ # Return the ID
+ return blob.id
def get(self, uuid):
- res = self.db.get("SELECT uuid, subject, time_created, time_expires, address, uid, \
- mimetype, views, size FROM nopaste WHERE uuid = %s AND (CASE WHEN time_expires \
- IS NULL THEN TRUE ELSE NOW() < time_expires END)", uuid)
-
- if res:
- # Get the account that uploaded this if available
- res.account = None
- if res.uid:
- res.account = self.backend.accounts.get_by_uid(res.uid)
-
- # Touch the entry so it won't be deleted when it is still used
- self._update_lastseen(uuid)
-
- return res
-
- def get_content(self, uuid):
- res = self.db.get("SELECT content FROM nopaste \
- WHERE uuid = %s", uuid)
-
- if res:
- return bytes(res.content)
-
- def _update_lastseen(self, uuid):
- self.db.execute("UPDATE nopaste SET time_lastseen = NOW(), views = views + 1 \
- WHERE uuid = %s", uuid)
-
- def _cleanup_database(self):
- # Delete old pastes when they are expired or when they have not been
- # accessed in a long time.
- self.db.execute("DELETE FROM nopaste WHERE (CASE \
- WHEN time_expires IS NULL \
- THEN time_lastseen + INTERVAL '6 months' <= NOW() \
- ELSE NOW() >= time_expires END)")
+ paste = self._get_paste("""
+ SELECT
+ *
+ FROM
+ nopaste
+ WHERE
+ uuid = %s
+ AND (
+ expires_at >= CURRENT_TIMESTAMP
+ OR
+ expires_at IS NULL
+ )
+ """, uuid,
+ )
+
+ return paste
+
+ def cleanup(self):
+ """
+ Removes all expired pastes and removes any unneeded blobs
+ """
+ # Remove all expired pastes
+ self.db.execute("""
+ DELETE FROM
+ nopaste
+ WHERE
+ expires_at < CURRENT_TIMESTAMP
+ """)
+
+ # Remove unneeded blobs
+ self.db.execute("""
+ DELETE FROM
+ nopaste_blobs
+ WHERE NOT EXISTS
+ (
+ SELECT
+ 1
+ FROM
+ nopaste
+ WHERE
+ nopaste.blob_id = nopaste_blobs.id
+ )
+ """)
+
+
+class Paste(Object):
+ def init(self, id, data):
+ self.id, self.data = id, data
+
+ def __str__(self):
+ return self.subject or self.uuid
+
+ # UUID
+
+ @property
+ def uuid(self):
+ return self.data.uuid
+
+ # Subject
+
+ @property
+ def subject(self):
+ return self.data.subject
+
+ # Created At
+
+ @property
+ def created_at(self):
+ return self.data.created_at
+
+ time_created = created_at
+
+ # Expires At
+
+ @property
+ def expires_at(self):
+ return self.data.expires_at
+
+ time_expires = expires_at
+
+ # Account
+
+ @lazy_property
+ def account(self):
+ return self.backend.accounts.get_by_uid(self.data.account)
+
+ # Blob
+
+ @lazy_property
+ def blob(self):
+ return self.backend.nopaste._fetch_blob(self.data.blob_id)
+
+ content = blob
+
+ # Size
+
+ @property
+ def size(self):
+ return self.data.size
+
+ # MIME Type
+
+ @property
+ def mimetype(self):
+ return self.data.mimetype or "application/octet-stream"
+
+ # Address
+
+ @property
+ def address(self):
+ return self.data.address
+
+ # Location
+
+ @lazy_property
+ def location(self):
+ return self.backend.location.lookup("%s" % self.address)
+
+ # ASN
+
+ @lazy_property
+ def asn(self):
+ if self.location and self.location.asn:
+ return self.backend.location.get_as(self.location.asn)
+
+ # Country
+
+ @lazy_property
+ def country(self):
+ if self.location and self.location.country_code:
+ return self.backend.location.get_country(self.location.country_code)
+
+ # Viewed?
+
+ def viewed(self):
+ """
+ Call this when this paste has been viewed/downloaded/etc.
+ """
+ self.db.execute("""
+ UPDATE
+ nopaste
+ SET
+ last_accessed_at = CURRENT_TIMESTAMP,
+ views = views + 1
+ WHERE
+ id = %s
+ """, self.id,
+ )