Files
stacks-puppet-node/blockstack_client/backend/drivers/disk.py

422 lines
11 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Blockstack-client
~~~~~
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
copyright: (c) 2016 by Blockstack.org
This file is part of Blockstack-client.
Blockstack-client is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Blockstack-client is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Blockstack-client. If not, see <http://www.gnu.org/licenses/>.
"""
# This module lets the blockstack client treat local disk as a storage provider.
# This is useful for doing local testing.
import os
import sys
import traceback
import logging
from common import get_logger, DEBUG
from ConfigParser import SafeConfigParser
log = get_logger("blockstack-storage-driver-disk")
DISK_ROOT = None
MUTABLE_STORAGE_ROOT = None
IMMUTABLE_STORAGE_ROOT = None
if os.environ.get("BLOCKSTACK_TEST", None) is not None:
DISK_ROOT = "/tmp/blockstack-disk"
else:
DISK_ROOT = os.path.expanduser("~/.blockstack/storage-disk")
log.setLevel( logging.DEBUG if DEBUG else logging.INFO )
def storage_init(conf):
"""
Local disk implementation of the storage_init API call.
Do one-time global setup--i.e. make directories.
Return True on success
Return False on error
"""
global DISK_ROOT, MUTABLE_STORAGE_ROOT, IMMUTABLE_STORAGE_ROOT
config_path = conf['path']
if os.path.exists( config_path ):
parser = SafeConfigParser()
try:
parser.read(config_path)
except Exception, e:
log.exception(e)
return False
if parser.has_section('disk'):
if parser.has_option('disk', 'root'):
DISK_ROOT = parser.get('disk', 'root')
if parser.has_option('disk', 'immutable'):
IMMUTABLE_STORAGE_ROOT = parser.get('disk', 'immutable')
if parser.has_option('disk', 'mutable'):
MUTABLE_STORAGE_ROOT = parser.get('disk', 'mutable')
if MUTABLE_STORAGE_ROOT is None:
MUTABLE_STORAGE_ROOT = os.path.join(DISK_ROOT, 'mutable')
if IMMUTABLE_STORAGE_ROOT is None:
IMMUTABLE_STORAGE_ROOT = os.path.join(DISK_ROOT, 'immutable')
if not os.path.isdir( DISK_ROOT ):
try:
os.makedirs( DISK_ROOT )
except:
pass
if not os.path.isdir( MUTABLE_STORAGE_ROOT ):
try:
os.makedirs( MUTABLE_STORAGE_ROOT )
except:
pass
if not os.path.isdir( IMMUTABLE_STORAGE_ROOT ):
try:
os.makedirs( IMMUTABLE_STORAGE_ROOT )
except:
pass
return True
def handles_url( url ):
"""
Does this storage driver handle this kind of URL?
"""
return url.startswith("file://")
def make_mutable_url( data_id ):
"""
Local disk implementation of the make_mutable_url API call.
Given the ID of the data, generate a URL that
can be used to route reads and writes to the data.
Return a string.
"""
global MUTABLE_STORAGE_ROOT
# replace all /'s with \x2f's
data_id_noslash = data_id.replace( "/", r"\x2f" )
return "file://%s/%s" % (MUTABLE_STORAGE_ROOT, data_id_noslash)
def get_immutable_handler( key, **kw ):
"""
Local disk implementation of the get_immutable_handler API call.
Given the hash of the data, return the data.
Return None if not found.
"""
global IMMUTABLE_STORAGE_ROOT
data = None
path = os.path.join( IMMUTABLE_STORAGE_ROOT, key )
if not os.path.exists(path):
if DEBUG:
log.debug("No such file or directory: '%s'" % path)
return None
try:
with open( path, "r" ) as f:
data = f.read()
return data
except Exception, e:
if DEBUG:
traceback.print_exc()
return None
def get_mutable_handler( url, **kw ):
"""
Local disk implementation of the get_mutable_handler API call.
Given a route URL to data, return the data itself.
Return the data if found.
Return None if not.
"""
if not url.startswith( "file://" ):
# invalid
return None
# get path from URL
path = url[ len("file://"): ]
if not os.path.exists(path):
if DEBUG:
log.debug("No such file or directory: '%s'" % path)
return None
try:
with open( path, "r" ) as f:
data = f.read()
return data
except Exception, e:
if DEBUG:
traceback.print_exc()
return None
def put_immutable_handler( key, data, txid, **kw ):
"""
Local disk implmentation of the put_immutable_handler API call.
Given the hash of the data (key), the serialized data itself,
and the transaction ID in the blockchain that contains the data's hash,
put the data into the storage system.
Return True on success; False on failure.
"""
global IMMUTABLE_STORAGE_ROOT, DEBUG
path = os.path.join( IMMUTABLE_STORAGE_ROOT, key )
pathdir = os.path.dirname(path)
if not os.path.exists(pathdir):
try:
os.makedirs(path_dir, 0700)
except Exception, e:
if DEBUG:
log.exception(e)
return False
try:
with open( path, "w") as f:
f.write( data )
f.flush()
if DEBUG:
log.debug("Stored to '%s'" % path)
except Exception, e:
if DEBUG:
traceback.print_exc()
return False
return True
def put_mutable_handler( data_id, data_bin, **kw ):
"""
Local disk implementation of the put_mutable_handler API call.
Return True on success; False on failure.
"""
global MUTABLE_STORAGE_ROOT, DEBUG
# replace all /'s with \x2f's
data_id_noslash = data_id.replace( "/", r"\x2f" )
path = os.path.join( MUTABLE_STORAGE_ROOT, data_id_noslash )
pathdir = os.path.dirname(path)
if not os.path.exists(pathdir):
try:
os.makedirs(path_dir, 0700)
except Exception, e:
if DEBUG:
log.exception(e)
return False
try:
with open( path, "w" ) as f:
f.write( data_bin )
f.flush()
if DEBUG:
log.debug("Stored to '%s'" % path)
except Exception, e:
if DEBUG:
log.exception(e)
return False
return True
def delete_immutable_handler( key, txid, sig_key_txid, **kw ):
"""
Local disk implementation of the delete_immutable_handler API call.
Given the hash of the data and transaction ID of the update
that deleted the data, remove data from storage.
Return True on success; False if not.
"""
global IMMUTABLE_STORAGE_ROOT
path = os.path.join( IMMUTABLE_STORAGE_ROOT, key )
try:
os.unlink( path )
except Exception, e:
pass
return True
def delete_mutable_handler( data_id, signature, **kw ):
"""
Local disk implementation of the delete_mutable_handler API call.
Given the unchanging data ID for the data and the writer's
signature over the hash of the data_id, remove data from storage.
Return True on success; False if not.
"""
global MUTABLE_STORAGE_ROOT
data_id_noslash = data_id.replace( "/", r"\x2f" )
path = os.path.join( MUTABLE_STORAGE_ROOT, data_id_noslash )
try:
os.unlink( path )
except Exception, e:
pass
return True
if __name__ == "__main__":
"""
Unit tests.
"""
import pybitcoin
import json
# hack around absolute paths
current_dir = os.path.abspath(os.path.join( os.path.dirname(__file__), "..") )
sys.path.insert(0, current_dir)
from storage import serialize_mutable_data, parse_mutable_data
from user import make_mutable_data_info
pk = pybitcoin.BitcoinPrivateKey()
data_privkey = pk.to_hex()
data_pubkey = pk.public_key().to_hex()
test_data = [
["my_first_datum", "hello world", 1, "unused", None],
["/my/second/datum", "hello world 2", 2, "unused", None],
["user_profile", '{"name":{"formatted":"judecn"},"v":"2"}', 3, "unused", None],
["empty_string", "", 4, "unused", None],
]
def hash_data( d ):
return pybitcoin.hash.hex_hash160( d )
rc = storage_init()
if not rc:
raise Exception("Failed to initialize")
# put_immutable_handler
print "put_immutable_handler"
for i in xrange(0, len(test_data)):
d_id, d, n, s, url = test_data[i]
rc = put_immutable_handler( hash_data( d ), d, "unused" )
if not rc:
raise Exception("put_immutable_handler('%s') failed" % d)
# put_mutable_handler
print "put_mutable_handler"
for i in xrange(0, len(test_data)):
d_id, d, n, s, url = test_data[i]
data_url = make_mutable_url( d_id )
data_zonefile = make_mutable_data_info( d_id, n, [data_url] )
data_json = serialize_mutable_data( {"id": d_id, "nonce": n, "data": d}, data_privkey )
rc = put_mutable_handler( d_id, data_json )
if not rc:
raise Exception("put_mutable_handler('%s', '%s') failed" % (d_id, d))
test_data[i][4] = data_url
# get_immutable_handler
print "get_immutable_handler"
for i in xrange(0, len(test_data)):
d_id, d, n, s, url = test_data[i]
rd = get_immutable_handler( hash_data( d ) )
if rd != d:
raise Exception("get_mutable_handler('%s'): '%s' != '%s'" % (hash_data(d), d, rd))
# get_mutable_handler
print "get_mutable_handler"
for i in xrange(0, len(test_data)):
d_id, d, n, s, url = test_data[i]
rd_json = get_mutable_handler( url )
rd = parse_mutable_data( rd_json, data_pubkey )
if rd is None:
raise Exception("Failed to parse mutable data '%s'" % rd_json)
if rd['id'] != d_id:
raise Exception("Data ID mismatch: '%s' != '%s'" % (rd['id'], d_id))
if rd['nonce'] != n:
raise Exception("Nonce mismatch: '%s' != '%s'" % (rd['nonce'], n))
if rd['data'] != d:
raise Exception("Data mismatch: '%s' != '%s'" % (rd['data'], d))
# delete_immutable_handler
print "delete_immutable_handler"
for i in xrange(0, len(test_data)):
d_id, d, n, s, url = test_data[i]
rc = delete_immutable_handler( hash_data(d), "unused", "unused" )
if not rc:
raise Exception("delete_immutable_handler('%s' (%s)) failed" % (hash_data(d), d))
# delete_mutable_handler
print "delete_mutable_handler"
for i in xrange(0, len(test_data)):
d_id, d, n, s, url = test_data[i]
rc = delete_mutable_handler( d_id, "unused" )
if not rc:
raise Exception("delete_mutable_handler('%s') failed" % d_id)