mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-03-30 16:45:26 +08:00
782 lines
25 KiB
Python
782 lines
25 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Blockstack-client
|
|
~~~~~
|
|
copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
|
|
copyright: (c) 2016-2017 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/>.
|
|
"""
|
|
import os
|
|
import re
|
|
import json
|
|
import hashlib
|
|
import urllib
|
|
import urllib2
|
|
import base64
|
|
import posixpath
|
|
|
|
import data
|
|
import app
|
|
import user as user_db
|
|
import wallet
|
|
import schemas
|
|
|
|
from config import get_logger
|
|
from constants import CONFIG_PATH, BLOCKSTACK_TEST, BLOCKSTACK_DEBUG
|
|
from scripts import is_name_valid, is_valid_hash
|
|
import string
|
|
|
|
class PasswordRequiredException(Exception):
|
|
pass
|
|
|
|
log = get_logger('blockstack-client')
|
|
|
|
B40_CHARS = string.digits + string.lowercase + '-_.+'
|
|
B40_CLASS = '[a-z0-9\-_.+]'
|
|
B40_NO_PERIOD_CLASS = '[a-z0-9\-_+]'
|
|
B40_REGEX = '^{}*$'.format(B40_CLASS)
|
|
URLENCODED_CLASS = '[a-zA-Z0-9\-_.~%]'
|
|
URLENCODED_PATH_CLASS = '[a-zA-Z0-9\-_.~%/]'
|
|
|
|
def _get_account_datastore_name(account_info):
|
|
"""
|
|
Get the name for an account datastore
|
|
"""
|
|
user_id = account_info['user_id']
|
|
app_fqu = account_info['name']
|
|
appname = account_info['appname']
|
|
|
|
datastore_name = app.app_account_datastore_name( app.app_account_name(user_id, app_fqu, appname) )
|
|
return datastore_name
|
|
|
|
|
|
def get_account_datastore_creds( account_info, user_privkey_hex ):
|
|
"""
|
|
Get an account datastore's name and private key
|
|
"""
|
|
datastore_privkey_hex = app.app_account_get_privkey( user_privkey_hex, account_info )
|
|
user_id = account_info['user_id']
|
|
datastore_name = _get_account_datastore_name(account_info)
|
|
|
|
return {'user_id': user_id, 'datastore_name': datastore_name, 'datastore_privkey': datastore_privkey_hex}
|
|
|
|
|
|
def get_account_datastore(account_info, proxy=None, config_path=CONFIG_PATH ):
|
|
"""
|
|
Get the datastore for the given account
|
|
@account_info is the account information
|
|
return {'status': True} on success
|
|
return {'error': ...} on failure
|
|
"""
|
|
user_id = account_info['user_id']
|
|
datastore_name = _get_account_datastore_name(account_info)
|
|
datastore_pubkey = str(account_info['public_key'])
|
|
log.debug("Get account datastore {}".format(datastore_name))
|
|
return data.get_datastore(user_id, datastore_name, datastore_pubkey, config_path=config_path, proxy=proxy )
|
|
|
|
|
|
def get_user_datastore(user_info, datastore_name, proxy=None, config_path=CONFIG_PATH ):
|
|
"""
|
|
Get the datastore for the given user
|
|
@account_info is the account information
|
|
return {'status': True} on success
|
|
return {'error': ...} on failure
|
|
"""
|
|
user_id = user_info['user_id']
|
|
datastore_pubkey = str(user_info['public_key'])
|
|
log.debug("Get user datastore {}".format(datastore_name))
|
|
return data.get_datastore(user_id, datastore_name, datastore_pubkey, config_path=config_path, proxy=proxy )
|
|
|
|
|
|
def get_account_datastore_info( master_data_privkey, user_id, app_fqu, app_name, config_path=CONFIG_PATH, proxy=None ):
|
|
"""
|
|
Get information about an account datastore.
|
|
At least, get the user and account owner.
|
|
|
|
Return {'status': True, 'user': user, 'user_privkey': ..., 'account': account, 'datastore': ..., 'datastore_privkey': ...} on success.
|
|
If master_data_privkey is not given, then user_privkey and datastore_privkey will not be provided.
|
|
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
master_data_pubkey = get_pubkey_hex(master_data_privkey)
|
|
|
|
# get user info
|
|
user_info = data.get_user(user_id, master_data_privkey, config_path=config_path)
|
|
if 'error' in user_info:
|
|
return user_info
|
|
|
|
if not user_info['owned']:
|
|
# we have to own this user, since this is an account-specific datastore
|
|
return {'error': 'This wallet does not own this user'}
|
|
|
|
user = user_info['user']
|
|
|
|
user_privkey_hex = user_db.user_get_privkey(master_data_privkey, user, config_path=config_path)
|
|
if user_privkey_hex is None:
|
|
return {'error': 'Failed to load user private key'}
|
|
|
|
res = app.app_load_account(user_id, app_fqu, app_name, user['public_key'], config_path=config_path)
|
|
if 'error' in res:
|
|
return res
|
|
|
|
acct = res['account']
|
|
|
|
res = get_account_datastore(acct, proxy=proxy, config_path=config_path)
|
|
if 'error' in res:
|
|
log.debug("Failed to get datastore for {}".format(user_id))
|
|
return res
|
|
|
|
datastore = res['datastore']
|
|
datastore_privkey_hex = None
|
|
|
|
if user_privkey_hex is not None:
|
|
datastore_privkey_hex = app.app_account_get_privkey( user_privkey_hex, acct )
|
|
if datastore_privkey_hex is None:
|
|
return {'error': 'Failed to load app account private key'}
|
|
|
|
|
|
ret = {
|
|
'user': user,
|
|
'account': acct,
|
|
'datastore': datastore,
|
|
'status': True
|
|
}
|
|
|
|
if user_privkey_hex is not None:
|
|
ret['user_privkey'] = user_privkey_hex
|
|
|
|
if datastore_privkey_hex is not None:
|
|
ret['datastore_privkey'] = datastore_privkey_hex
|
|
|
|
return ret
|
|
|
|
|
|
def get_user_datastore_info( master_data_privkey, user_id, datastore_name, config_path=CONFIG_PATH, proxy=None ):
|
|
"""
|
|
Get information about a datastore that belongs directly to a user (without an account)
|
|
If master_data_privkey is not None, then also get the datastore private key.
|
|
|
|
Return {'status': True, 'user': user, 'user_privkey': ..., 'datastore': ..., 'datastore_privkey': ...} on success.
|
|
If master_data_privkey is not given, then user_privkey and datastore_privkey will not be provided.
|
|
|
|
Return {'error': ...} on failure
|
|
"""
|
|
|
|
master_data_pubkey = get_pubkey_hex(master_data_privkey)
|
|
|
|
res = data.get_user(user_id, master_data_privkey, config_path=config_path)
|
|
if 'error' in res:
|
|
return res
|
|
|
|
user = res['user']
|
|
user_pubkey = user['public_key']
|
|
|
|
user_privkey_hex = user_db.user_get_privkey(master_data_privkey, user, config_path=config_path)
|
|
if user_privkey_hex is None:
|
|
return {'error': 'Failed to load user private key'}
|
|
|
|
res = get_user_datastore(user, datastore_name, proxy=proxy, config_path=config_path)
|
|
if 'error' in res:
|
|
return res
|
|
|
|
datastore = res['datastore']
|
|
datastore_privkey_hex = user_privkey_hex
|
|
|
|
ret = {
|
|
'user': user,
|
|
'datastore': datastore,
|
|
'status': True
|
|
}
|
|
|
|
if datastore_privkey_hex is not None:
|
|
ret['datastore_privkey'] = datastore_privkey_hex
|
|
|
|
return ret
|
|
|
|
|
|
def get_datastore_name_info( user_id, datastore_id ):
|
|
"""
|
|
Parse a datastore ID into an application blockchain ID and name, if
|
|
the datastore ID refers to an account-owned datastore.
|
|
|
|
Return {'app_fqu': app_fqu, 'appname': appname, 'datastore_name': datastore_name} on success
|
|
Return {'error': ...} on error
|
|
"""
|
|
account_name_parts = app.app_account_parse_datastore_name(datastore_id)
|
|
app_fqu = None
|
|
appname = None
|
|
datastore_name = None
|
|
|
|
if account_name_parts is not None:
|
|
# this is an account-specific datastore
|
|
if user_id != account_name_parts['user_id']:
|
|
return {'error': 'Invalid user ID for given data store name'}
|
|
|
|
app_fqu = account_name_parts['app_blockchain_id']
|
|
appname = account_name_parts['app_name']
|
|
|
|
else:
|
|
# this is a generic datastore
|
|
datastore_name = datastore_id
|
|
|
|
return {'app_fqu': app_fqu, 'appname': appname, 'datastore_name': datastore_name}
|
|
|
|
|
|
def get_datastore_info( user_id, datastore_id, wallet_keys, include_private=False, config_path=CONFIG_PATH, proxy=None, password=None ):
|
|
"""
|
|
Get datastore information. If the datastore information is not locally hosted, then user_id must be a blockchain ID that points to the
|
|
zone file with the master public key.
|
|
|
|
Returns {
|
|
'datastore': datastore record,
|
|
'datastore_privkey': datastore private key (if include_private is True and we have the ciphertext locally). Hex-encoded
|
|
'app_fqu': name that points to owner of the application for which this datastore holds the user's data (if defined)
|
|
'appname': name of application for which this datastore holds the user's data the datastore (if defined)
|
|
'datastore_name': name of datastore
|
|
'master_data_pubkey': master data public key
|
|
'master_data_privkey': master data private key (only given if include_private is True)
|
|
}
|
|
|
|
Returns {'error': ...} on error
|
|
"""
|
|
|
|
if proxy is None:
|
|
proxy = get_default_proxy(config_path)
|
|
|
|
config_dir = os.path.dirname(config_path)
|
|
|
|
account_name_parts = app.app_account_parse_datastore_name(datastore_id)
|
|
app_fqu = None
|
|
appname = None
|
|
datastore_name = None
|
|
master_data_privkey = None
|
|
datastore_privkey_hex = None
|
|
|
|
name_info = get_datastore_name_info(user_id, datastore_id)
|
|
if 'error' in name_info:
|
|
# user ID mismatch
|
|
return name_info
|
|
|
|
app_fqu = name_info['app_fqu']
|
|
appname = name_info['appname']
|
|
datastore_name = name_info['datastore_name']
|
|
|
|
assert wallet_keys
|
|
assert wallet_keys.has_key('data_privkey')
|
|
master_data_privkey = wallet_keys['data_privkey']
|
|
master_data_pubkey = get_pubkey_hex(master_data_privkey)
|
|
|
|
datastore_info = None
|
|
datastore = None
|
|
|
|
if app_fqu is not None and appname is not None:
|
|
log.debug("Datastore {} is an account datastore".format(datastore_id))
|
|
datastore_info = get_account_datastore_info( master_data_privkey, user_id, app_fqu, appname, config_path=config_path, proxy=proxy )
|
|
|
|
else:
|
|
log.debug("Datastore {} is a user datastore".format(datastore_id))
|
|
datastore_info = get_user_datastore_info( master_data_privkey, user_id, datastore_name, config_path=config_path, proxy=proxy )
|
|
|
|
if 'error' in datastore_info:
|
|
log.error("Failed to get datastore information")
|
|
return datastore_info
|
|
|
|
datastore = datastore_info['datastore']
|
|
if include_private:
|
|
datastore_privkey_hex = datastore_info['datastore_privkey']
|
|
|
|
ret = {
|
|
'datastore': datastore,
|
|
'datastore_privkey': datastore_privkey_hex,
|
|
'datastore_info': datastore_info,
|
|
'app_fqu': app_fqu,
|
|
'appname': appname,
|
|
'datastore_name': datastore_name,
|
|
'master_data_pubkey': master_data_pubkey,
|
|
}
|
|
|
|
if include_private:
|
|
ret['master_data_privkey'] = master_data_privkey
|
|
|
|
return ret
|
|
|
|
|
|
def blockstack_mutable_data_url(blockchain_id, data_id, version):
|
|
"""
|
|
Make a blockstack:// URL for mutable data
|
|
data_id must be url-quoted
|
|
"""
|
|
assert re.match(schemas.OP_URLENCODED_PATTERN, data_id)
|
|
|
|
if version is None:
|
|
return 'blockstack://{}/{}'.format(
|
|
urllib.quote(blockchain_id), data_id
|
|
)
|
|
|
|
if not isinstance(version, (int, long)):
|
|
raise ValueError('Verison must be an int or long')
|
|
|
|
return 'blockstack://{}/{}#{}'.format(
|
|
urllib.quote(blockchain_id), data_id, str(version)
|
|
)
|
|
|
|
|
|
def blockstack_immutable_data_url(blockchain_id, data_id, data_hash):
|
|
"""
|
|
Make a blockstack:// URL for immutable data
|
|
data_id must be url-quoted
|
|
"""
|
|
assert re.match(schemas.OP_URLENCODED_PATTERN, data_id)
|
|
|
|
if data_hash is not None and not is_valid_hash(data_hash):
|
|
raise ValueError('Invalid hash: {}'.format(data_hash))
|
|
|
|
if data_hash is not None:
|
|
return 'blockstack://{}.{}/#{}'.format(
|
|
data_id, urllib.quote(blockchain_id), data_hash
|
|
)
|
|
|
|
return 'blockstack://{}.{}'.format(
|
|
data_id, urllib.quote(blockchain_id)
|
|
)
|
|
|
|
|
|
def blockstack_datastore_url( user_id, datastore_id, path ):
|
|
"""
|
|
Make a blockstack:// URL for a datastore record
|
|
"""
|
|
assert re.match(schemas.OP_URLENCODED_PATTERN, user_id)
|
|
assert re.match(schemas.OP_URLENCODED_PATTERN, datastore_id)
|
|
|
|
path = '/'.join( [urllib.quote(p) for p in posixpath.normpath(path).split('/')] )
|
|
|
|
return 'blockstack://{}@{}/{}'.format(urllib.quote(datastore_id), urllib.quote(user_id), path)
|
|
|
|
|
|
def blockstack_mutable_data_url_parse(url):
|
|
"""
|
|
Parse a blockstack:// URL for mutable data
|
|
Return (blockchain ID, data ID, data version, user ID, datastore ID) on success.
|
|
The data ID will be a path if user ID and datastore ID are given; if the path ends in '/', then a directory is specifically requested.
|
|
The version may be None if not given (in which case, the latest value is requested).
|
|
"""
|
|
|
|
url = str(url)
|
|
mutable_url_data_regex = r'^blockstack://({}+)[/]+({}+)[/]*(#[0-9]+)?$'.format(B40_CLASS, URLENCODED_CLASS)
|
|
datastore_url_data_regex = r"^blockstack://({}+)@({}+)[/]+({}+)$".format(schemas.OP_DATASTORE_ID_CLASS, schemas.OP_USER_ID_CLASS, URLENCODED_PATH_CLASS)
|
|
|
|
blockchain_id, data_id, version, user_id, datastore_id = None, None, None, None, None
|
|
is_dir = False
|
|
|
|
# mutable?
|
|
m = re.match(mutable_url_data_regex, url)
|
|
if m:
|
|
|
|
blockchain_id, data_id, version = m.groups()
|
|
if not is_name_valid(blockchain_id):
|
|
raise ValueError('Invalid blockchain ID "{}"'.format(blockchain_id))
|
|
|
|
# version?
|
|
if version is not None:
|
|
version = version.strip('#/')
|
|
version = int(version)
|
|
|
|
return urllib.unquote(blockchain_id), data_id, version, None, None
|
|
|
|
# datastore?
|
|
m = re.match(datastore_url_data_regex, url)
|
|
if m:
|
|
|
|
datastore_id, user_id, path = m.groups()
|
|
if path.endswith('/'):
|
|
is_dir = True
|
|
|
|
# unquote
|
|
path = '/' + '/'.join([urllib.unquote(p) for p in posixpath.normpath(path).split('/')])
|
|
if is_dir:
|
|
path += '/'
|
|
|
|
return None, urllib.unquote(path), version, user_id, datastore_id
|
|
|
|
return None, None, None, None, None
|
|
|
|
|
|
def blockstack_immutable_data_url_parse(url):
|
|
"""
|
|
Parse a blockstack:// URL for immutable data
|
|
Return (blockchain ID, data ID, data hash)
|
|
* The hash may be None if not given, in which case, the hash should be looked up from the blockchain ID's profile.
|
|
* The data ID may be None, in which case, the list of immutable data is requested.
|
|
|
|
Raise on bad data
|
|
"""
|
|
|
|
url = str(url)
|
|
immutable_data_regex = r'^blockstack://({}+)\.({}+)\.({}+)[/]*([/]+#[a-fA-F0-9]+)?$'.format(
|
|
URLENCODED_CLASS, B40_NO_PERIOD_CLASS, B40_NO_PERIOD_CLASS
|
|
)
|
|
immutable_listing_regex = r'^blockstack://({}+)[/]+#immutable$'.format(B40_CLASS)
|
|
|
|
m = re.match(immutable_data_regex, url)
|
|
if m:
|
|
data_id, blockchain_name, namespace_id, data_hash = m.groups()
|
|
blockchain_id = '{}.{}'.format(blockchain_name, namespace_id)
|
|
|
|
if not is_name_valid(blockchain_id):
|
|
log.debug('Invalid blockstack ID "{}"'.format(blockchain_id))
|
|
raise ValueError('Invalid blockstack ID')
|
|
|
|
if data_hash is not None:
|
|
data_hash = data_hash.lower().strip('#/')
|
|
if not is_valid_hash(data_hash):
|
|
log.debug('Invalid data hash "{}"'.format(data_hash))
|
|
raise ValueError('Invalid data hash')
|
|
|
|
return urllib.unquote(blockchain_id), data_id, data_hash
|
|
else:
|
|
# maybe a listing?
|
|
m = re.match(immutable_listing_regex, url)
|
|
if not m:
|
|
log.debug('Invalid immutable URL "{}"'.format(url))
|
|
raise ValueError('Invalid immutable URL')
|
|
|
|
blockchain_id = m.groups()[0]
|
|
return urllib.unquote(blockchain_id), None, None
|
|
|
|
return None, None, None
|
|
|
|
|
|
def blockstack_data_url_parse(url):
|
|
"""
|
|
Parse a blockstack:// URL
|
|
Return {
|
|
'type': immutable|mutable
|
|
'blockchain_id': blockchain ID
|
|
'data_id': data_id
|
|
'fields': { fields }
|
|
} on success
|
|
Fields will be either {'data_hash'} on immutable
|
|
or {'version'} on mutable
|
|
|
|
Return None on error
|
|
"""
|
|
|
|
blockchain_id, data_id, url_type = None, None, None
|
|
fields = {}
|
|
|
|
try:
|
|
blockchain_id, data_id, data_hash = blockstack_immutable_data_url_parse(url)
|
|
assert blockchain_id is not None
|
|
|
|
url_type = 'immutable'
|
|
fields.update({'data_hash': data_hash})
|
|
|
|
log.debug("Immutable data URL: {}".format(url))
|
|
|
|
except (ValueError, AssertionError) as e1:
|
|
log.debug("Not an immutable data URL: {}".format(url))
|
|
|
|
try:
|
|
blockchain_id, data_id, version, user_id, datastore_id = (
|
|
blockstack_mutable_data_url_parse(url)
|
|
)
|
|
|
|
url_type = 'mutable'
|
|
assert (blockchain_id is None and user_id is not None and datastore_id is not None) or (blockchain_id is not None and user_id is None and datastore_id is None)
|
|
|
|
if blockchain_id is not None:
|
|
fields['version'] = version
|
|
|
|
else:
|
|
fields['datastore_id'] = datastore_id
|
|
fields['user_id'] = user_id
|
|
|
|
log.debug("Mutable data URL: {}".format(url))
|
|
|
|
except (ValueError, AssertionError) as e2:
|
|
if BLOCKSTACK_TEST:
|
|
log.exception(e2)
|
|
|
|
log.debug('Unparseable URL "{}"'.format(url))
|
|
return None
|
|
|
|
ret = {
|
|
'type': url_type,
|
|
'blockchain_id': blockchain_id,
|
|
'data_id': data_id,
|
|
'fields': fields
|
|
}
|
|
|
|
return ret
|
|
|
|
|
|
def blockstack_data_url(field_dict):
|
|
"""
|
|
Make a blockstack:// URL from constituent fields.
|
|
Takes the output of blockstack_data_url_parse
|
|
Return the URL on success
|
|
Raise on error
|
|
"""
|
|
assert 'blockchain_id' in field_dict
|
|
assert 'type' in field_dict
|
|
assert field_dict['type'] in ['mutable', 'immutable']
|
|
assert 'data_id' in field_dict
|
|
assert 'fields' in field_dict
|
|
assert 'data_hash' in field_dict['fields'] or 'version' in field_dict['fields']
|
|
|
|
if field_dict['type'] == 'immutable':
|
|
return blockstack_immutable_data_url(
|
|
field_dict['blockchain_id'], field_dict['data_id'], field_dict['fields']['data_hash']
|
|
)
|
|
|
|
if field_dict['fields'].has_key('user_id') and field_dict['fields'].has_key('datastore_id'):
|
|
return blockstack_datastore_url(
|
|
field_dict['fields']['user_id'], field_dict['fields']['datastore_id'], field_dict['data_id']
|
|
)
|
|
|
|
return blockstack_mutable_data_url(
|
|
field_dict['blockchain_id'], field_dict['data_id'], field_dict['fields']['version']
|
|
)
|
|
|
|
|
|
def blockstack_url_fetch(url, proxy=None, config_path=CONFIG_PATH, wallet_keys=None):
|
|
"""
|
|
Given a blockstack:// url, fetch its data.
|
|
If the data is an immutable data url, and the hash is not given, then look up the hash first.
|
|
If the data is a mutable data url, and the version is not given, then look up the version as well.
|
|
|
|
Data from datastores requires wallet_keys
|
|
|
|
Return {"data": data} on success
|
|
Return {"error": error message} on error
|
|
"""
|
|
mutable = False
|
|
immutable = False
|
|
blockchain_id = None
|
|
data_id = None
|
|
version = None
|
|
data_hash = None
|
|
user_id = None
|
|
datastore_id = None
|
|
|
|
url_info = blockstack_data_url_parse(url)
|
|
if url_info is None:
|
|
return {'error': 'Failed to parse {}'.format(url)}
|
|
|
|
data_id = url_info['data_id']
|
|
blockchain_id = url_info['blockchain_id']
|
|
url_type = url_info['type']
|
|
fields = url_info['fields']
|
|
|
|
if url_type == 'mutable':
|
|
version = fields.get('version')
|
|
user_id = fields.get('user_id')
|
|
datastore_id = fields.get('datastore_id')
|
|
mutable = True
|
|
|
|
else:
|
|
data_hash = fields.get('data_hash')
|
|
immutable = True
|
|
|
|
if mutable:
|
|
if user_id is not None and datastore_id is not None:
|
|
# get from datastore
|
|
if wallet_keys is None:
|
|
raise PasswordRequiredException("need wallet keys to access data stores")
|
|
|
|
assert wallet_keys, "Need wallet keys to access data stores"
|
|
datastore_info = get_datastore_info(user_id, datastore_id, config_path=config_path, proxy=proxy, wallet_keys=wallet_keys)
|
|
if 'error' in datastore_info:
|
|
return datastore_info
|
|
|
|
datastore = datastore_info['datastore']
|
|
|
|
# file or directory?
|
|
is_dir = data_id.endswith('/')
|
|
if is_dir:
|
|
return data.datastore_listdir( datastore, data_id, config_path=config_path, proxy=proxy )
|
|
else:
|
|
return data.datastore_getfile( datastore, data_id, config_path=config_path, proxy=proxy )
|
|
|
|
elif blockchain_id is not None:
|
|
# get single data
|
|
if version is not None:
|
|
return data.get_mutable( data_id, proxy=proxy, ver_min=version, ver_max=version+1, blockchain_id=blockchain_id, fully_qualified_data_id=True )
|
|
else:
|
|
return data.get_mutable( data_id, proxy=proxy, blockchain_id=blockchain_id, fully_qualified_data_id=True )
|
|
|
|
else:
|
|
return {'error': 'Invalid URL'}
|
|
|
|
else:
|
|
if data_id is not None:
|
|
# get single data
|
|
if data_hash is not None:
|
|
return data.get_immutable( blockchain_id, data_hash, data_id=data_id, proxy=proxy )
|
|
|
|
else:
|
|
return data.get_immutable_by_name( blockchain_id, data_id, proxy=proxy )
|
|
|
|
else:
|
|
# list data
|
|
return data.list_immutable_data( blockchain_id, proxy=proxy, config_path=config_path )
|
|
|
|
|
|
class BlockstackURLHandle(object):
|
|
"""
|
|
A file-like object that handles reads on blockstack URLs
|
|
"""
|
|
|
|
def __init__(self, url, data=None, full_response=False, config_path=CONFIG_PATH, wallet_keys=None):
|
|
self.name = url
|
|
self.data = data
|
|
self.full_response = full_response
|
|
self.fetched = False
|
|
self.config_path = config_path
|
|
self.wallet_keys = wallet_keys
|
|
|
|
self.offset = 0
|
|
self.closed = False
|
|
self.softspace = 0
|
|
|
|
if data is None:
|
|
self.newlines = None
|
|
else:
|
|
self.data_len = len(data)
|
|
self.fetched = True
|
|
self.newlines = self.make_newlines(data)
|
|
|
|
def make_newlines(self, data):
|
|
"""
|
|
Set up newlines
|
|
"""
|
|
|
|
return tuple(nls for nls in ('\n', '\r', '\r\n') if nls in data)
|
|
|
|
def fetch(self):
|
|
"""
|
|
Lazily fetch the data on read
|
|
"""
|
|
|
|
if not self.fetched:
|
|
from .proxy import get_default_proxy
|
|
|
|
proxy = get_default_proxy(config_path=self.config_path)
|
|
data = blockstack_url_fetch(
|
|
self.name, proxy=proxy, config_path=self.config_path
|
|
)
|
|
|
|
if data is None:
|
|
msg = 'Failed to fetch "{}"'
|
|
raise urllib2.URLError(msg.format(self.name))
|
|
|
|
if 'error' in data:
|
|
msg = 'Failed to fetch "{}": {}'
|
|
raise urllib2.URLError(msg.format(self.name, data['error']))
|
|
|
|
if self.full_response:
|
|
self.data = json.dumps(data)
|
|
else:
|
|
self.data = data['data']
|
|
if not isinstance(self.data, (str, unicode)):
|
|
self.data = json.dumps(data['data'])
|
|
|
|
self.newlines = self.make_newlines(data)
|
|
self.data_len = len(self.data)
|
|
self.fetched = True
|
|
|
|
def close(self):
|
|
self.data = None
|
|
self.closed = True
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def next(self):
|
|
line = self.readline()
|
|
if len(line) == 0:
|
|
raise StopIteration()
|
|
else:
|
|
return line
|
|
|
|
def read(self, numbytes=None):
|
|
self.fetch()
|
|
if self.offset >= self.data_len:
|
|
return ''
|
|
|
|
ret = []
|
|
if numbytes is not None:
|
|
ret = self.data[self.offset:min(self.data_len, self.offset + numbytes)]
|
|
self.offset += numbytes
|
|
self.offset = self.data_len if self.offset > self.data_len else self.offset
|
|
else:
|
|
ret = self.data[self.offset:]
|
|
self.offset = self.data_len
|
|
self.data = None
|
|
|
|
return ret
|
|
|
|
def readline(self, numbytes=None):
|
|
if self.data is None:
|
|
return ''
|
|
|
|
next_newline_offset = self.data[self.offset:].find('\n')
|
|
if next_newline_offset < 0:
|
|
# no more newlines
|
|
return self.read()
|
|
else:
|
|
line_data = self.read(next_newline_offset + 1)
|
|
return line_data
|
|
|
|
def readlines(self, sizehint=None):
|
|
sizehint = self.data_len if sizehint is None else sizehint
|
|
|
|
total_len = 0
|
|
lines = []
|
|
while total_len < sizehint:
|
|
line = self.readline()
|
|
lines.append(line)
|
|
total_len += len(line)
|
|
|
|
return lines
|
|
|
|
|
|
class BlockstackHandler(urllib2.BaseHandler):
|
|
"""
|
|
URL opener for blockstack:// URLs.
|
|
Usable with urllib2.
|
|
"""
|
|
|
|
def __init__(self, full_response=False, config_path=CONFIG_PATH):
|
|
self.full_response = full_response
|
|
self.config_path = config_path
|
|
|
|
def blockstack_open(self, req):
|
|
"""
|
|
Open a blockstack URL
|
|
"""
|
|
bh = BlockstackURLHandle(
|
|
req.get_full_url(), full_response=self.full_response,
|
|
config_path=self.config_path
|
|
)
|
|
|
|
return bh
|