summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2017-02-16 02:37:28 +0100
committerdrebs <drebs@leap.se>2017-04-04 18:27:17 +0200
commitf36a105e7480aed6c5e9e2c681e732fb7069301b (patch)
treeccf53d0aeb2b2edca2ecc65334197a34ea8de30c
parent109608e8c1051e8569f06f02f477412d16e4ef66 (diff)
[feature] working naive implementation of the backend
- Resolves: #8757, #8771, #8772
-rw-r--r--server/src/leap/soledad/server/_blobs.py142
-rw-r--r--server/src/leap/soledad/server/_resource.py4
2 files changed, 141 insertions, 5 deletions
diff --git a/server/src/leap/soledad/server/_blobs.py b/server/src/leap/soledad/server/_blobs.py
index cacabbdf..03d37059 100644
--- a/server/src/leap/soledad/server/_blobs.py
+++ b/server/src/leap/soledad/server/_blobs.py
@@ -14,27 +14,147 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
"""
Blobs Server implementation.
+
+This is a very simplistic implementation for the time being.
+Clients should be able to opt-in util the feature is complete.
+
+A more performant BlobsBackend can (and should) be implemented for production
+environments.
"""
+
+import os
+
+from twisted.web import static
from twisted.web import resource
+from twisted.web.client import FileBodyProducer
+from twisted.web.server import NOT_DONE_YET
+from zope.interface import Interface, implementer
from ._config import get_config
__all__ = ['BlobsResource', 'blobs_resource']
+# TODO some error handling needed
+# [ ] make path configurable
+# [ ] sanitize path
+# [ ] implement basic quota (and raise a QuotaExceeded if limit reached!)
+
+# for the future:
+# [ ] isolate user avatar in a safer way
+# [ ] catch timeout in the server (and delete incomplete upload)
+# p [ chunking (should we do it on the client or on the server?)
+
+
+class BlobAlreadyExists(Exception):
+ pass
+
+
+class IBlobsBackend(Interface):
+
+ """
+ An interface for a BlobsBackend.
+ """
+
+ def read_blob(user, blob_id, request):
+ """
+ Read blob with a given blob_id, and write it to the passed request.
+
+ :returns: a deferred that fires upon finishing.
+ """
+
+ def write_blob(user, blob_id, request):
+ """
+ Write blob to the storage, reading it from the passed request.
+
+ :returns: a deferred that fires upon finishing.
+ """
+
+ # other stuff for the API
+
+ def delete_blob(user, blob_id):
+ pass
+
+ def get_blob_size(user, blob_id):
+ pass
+
+ def get_total_storage(user):
+ pass
+
+
+@implementer(IBlobsBackend)
+class FilesystemBlobsBackend(object):
+
+ path = '/tmp/blobs/'
+
+ def read_blob(self, user, blob_id, request):
+ print "USER", user
+ print "BLOB_ID", blob_id
+ path = self.get_path(user, blob_id)
+ print "READ FROM", path
+ _file = static.File(path, defaultType='application/octet-stream')
+ return _file.render_GET(request)
+
+ def write_blob(self, user, blob_id, request):
+ path = self.get_path(user, blob_id)
+ if os.path.isfile(path):
+ raise BlobAlreadyExists()
+ try:
+ os.makedirs(os.path.split(path)[0])
+ except:
+ pass
+ print "WRITE TO", path
+ fbp = FileBodyProducer(request.content)
+ d = fbp.startProducing(open(path, 'wb'))
+ d.addCallback(lambda _: request.finish())
+ return NOT_DONE_YET
+
+ def get_path(self, user, blob_id):
+ parts = [user]
+ parts += [blob_id[0], blob_id[0:3], blob_id[0:6]]
+ parts += [blob_id]
+ return os.path.join(self.path, *parts)
+
+
class BlobsResource(resource.Resource):
isLeaf = True
+ # Allowed factory classes are defined here
+ blobsFactoryClass = FilesystemBlobsBackend
+
def __init__(self, blobs_path):
- resource.Resource.__init__(self)
- self._blobs_path = blobs_path
+ # TODO pass the backend as configurable option
+ """
+ __init__(self, backend, opts={})
+ factorykls = getattr(self, backend + 'Class')(**opts)
+ self._handler = kls()
+ """
+ resource.Resource.__init__(self)
+ self._blobs_path = blobs_path
+ self._handler = self.blobsFactoryClass()
+ assert IBlobsBackend.providedBy(self._handler)
+
+ # TODO double check credentials, we can have then
+ # under request.
def render_GET(self, request):
- return 'blobs is not implemented yet!'
+ user, blob_id = self._split_path(request.path)
+ return self._handler.read_blob(user, blob_id, request)
+
+ def render_PUT(self, request):
+ user, blob_id = self._split_path(request.path)
+ return self._handler.write_blob(user, blob_id, request)
+
+ def _split_path(self, blob_id):
+ # FIXME catch errors here
+ parts = blob_id.split('/')
+ _, user, blobname = parts
+ return user, blobname
# provide a configured instance of the resource
@@ -42,3 +162,19 @@ _config = get_config()
_path = _config['blobs_path']
blobs_resource = BlobsResource(_path)
+if __name__ == '__main__':
+ # A dummy blob server
+ # curl -X PUT --data-binary @/tmp/book.pdf localhost:9000/user/somerandomstring
+ # curl -X GET -o /dev/null localhost:9000/user/somerandomstring
+
+ from twisted.web.server import Site
+ from twisted.internet import reactor
+
+ # XXX pass the path here
+ root = BlobsResource()
+ # I picture somethink like
+ # BlobsResource(backend="filesystem", backend_opts={'path': '/tmp/blobs'})
+
+ factory = Site(root)
+ reactor.listenTCP(9000, factory)
+ reactor.run()
diff --git a/server/src/leap/soledad/server/_resource.py b/server/src/leap/soledad/server/_resource.py
index 7a00ad9a..04bb4fba 100644
--- a/server/src/leap/soledad/server/_resource.py
+++ b/server/src/leap/soledad/server/_resource.py
@@ -19,7 +19,7 @@ A twisted resource that serves the Soledad Server.
"""
from twisted.web.resource import Resource
-from ._blobs import blobs_resource
+from ._blobs import BlobsResource
from ._server_info import ServerInfo
from ._wsgi import get_sync_resource
@@ -74,7 +74,7 @@ class SoledadResource(Resource):
# requests to /blobs will serve blobs if enabled
if enable_blobs:
- self.putChild('blobs', blobs_resource)
+ self.putChild('blobs', BlobsResource())
# other requests are routed to legacy sync resource
self._sync_resource = get_sync_resource(sync_pool)