# coding:utf-8
"""
REST API for download
Copyright (C) 2020 JASRI All Rights Reserved.
"""
import codecs
import os
import sys
import json
import requests
import zipfile
if sys.version_info.major <=2:
import urlparse # python2
else:
import urllib.parse as urlparse # python3
from . import config
from . import util
[docs]class Main():
def __init__(self, parent):
self.__parent = parent
self.__endpoint = parent.endpoint(config.download_path)
self.queue = Queue(self)
self.file = File(self)
self.metalink = Metalink(self)
[docs] def endpoint(self, path):
return self.__parent.endpoint(path)
[docs] def access_token(self):
return self.__parent.access_token()
[docs] def post(self, **v):
"""
REST API: ZIPダウンロード命令実行 (非同期処理)
* Endpoint: [agent-url]/benten/v1/download
* Method: POST
* Response: JSON
* Authorization: 要
:param \**v:
See below.
:Keyword Arguments:
* *register_name_list* (``list(str)`` [Option]) : 登録名リスト
* *file_list* (``list(file)`` [Option]) : ディレクトリ及びファイルリスト
* ディレクトリとファイルの区別なし
* *flag_recursive* (``int`` [Option]) : ディレクトリ指定時に下位ディレクトリを含めてダウンロードするかどうかを示すフラグ
* デフォルト値は0, 1の時に有効になる
* *flag_own* (``int`` [Option]) : 自身の所属のデータのみダウンロードするかどうかのフラグ
* 0 (default, disable) or 1 (enable)
* *mode* (``str`` [Option]) : ZIP圧縮のモード (normal, stored, deflated)
* normal(default)の場合 file capttionが binaryを除いてZIP圧縮を行う。
* stored の場合、 ZIP無圧縮
* deflatedの場合、 ZIP圧縮
* *timeout* (``int``) : タイムアウト値 (秒単位)
* 最大値は 30
* 負の値の際は30がセットさあれる
(注) register_name_list または file_list のいずれかの入力要
:return:
A dict of mapping keys.
:Keyword:
* *status* (``str``) : ダウンロード実行状況
* 未完の場合は PENDING 、処理が開始した場合は STARTED、処理が失敗した場合は FAILURE、処理が終了し成功した場合は SUCCESS
* *uuid* (``str``) : ダウンロードを行うまたは状況確認の際に必要となるid値
* *error* (``dict``) : エラー情報 (エラー発生時のみ付加)
"""
vdata = {}
for key in ["register_name_list", "file_list", "flag_recursive", "flag_own", "mode", "timeout"]:
if key in v:
vdata[key] = v[key]
ret = requests.post(self.__endpoint, vdata, verify=False,
headers=util.headers_authorization(self.__parent.access_token()))
return util.json_response(ret)
[docs]class Queue():
def __init__(self, parent):
self.__parent = parent
self.__endpoint = parent.endpoint(config.download_queue_path)
[docs] def get(self, v):
"""
REST API: ZIPダウンロード命令のqueue確認
* Endpoint: [agent-url]/benten/v1/download/queue/<uuid>
* Method: GET
* Response: JSON
* Authorization: 不要
:param \**v:
指定なし
:return:
A dict of mapping keys.
:Keyword:
* *status* (``str``) : ダウンロード実行状況
* 未完の場合は PENDING 、処理が開始した場合は STARTED、処理が失敗した場合は FAILURE、処理が終了し成功した場合は SUCCESS
* *uuid* (``str``) : ダウンロードを行うまたは状況確認の際に必要となるid値
* *error* (``dict``) : エラー情報 (エラー発生時のみ付加)
"""
endpoint = "%s/%s" % (self.__endpoint, v)
ret = requests.get(endpoint, verify=False)
return util.json_response(ret)
[docs]class File():
def __init__(self, parent):
self.__parent = parent
self.__endpoint = parent.endpoint(config.download_file_path)
[docs] def get(self, v, out_directory=None, debug=True, unzip=False,):
"""
REST API: ZIPファイルダウンロード実行
* Endpoint: [agent-url]/benten/v1/download/file/<uuid>
* Method: GET
* Response: HTTP (JSON if error occured)
* Authorization: 必要
:param \**v:
指定なし
:param debug:
True の時に debug print される (Default値はTrue)
:param unzip:
Trueの際は、ダウンロード後にzipファイルを解凍する
:return:
なし
(注) Rest APIの返答においてContent-Disposition の header に記載されたzipファイル名で ファイルコンテンツのダウンロードを行う。
"""
endpoint = "%s/%s" % (self.__endpoint, v)
ret = requests.get(endpoint, verify=False,
headers=util.headers_authorization(self.__parent.access_token()),stream=True)
if ret.status_code != 200:
message = "HTTP status: " + str(ret.status_code)
raise util.Error(message, domain=util.error_domain(
__file__, sys._getframe()))
content_type = ret.headers["content-type"]
try:
content_length = int(ret.headers.get("Content-Length"))
except:
content_length = None
if "json" in content_type:
return util.json_response(ret)
if "zip" not in content_type:
message = "Content-Type: " + content_type
raise util.Error(message, domain=util.error_domain(
__file__, sys._getframe()))
try:
content_disposition = ret.headers["Content-Disposition"]
filename = content_disposition.split("filename=")[1].strip()
if out_directory is not None:
filename = "{}/{}".format(out_directory, filename)
util.makedirs(out_directory)
except:
raise util.Error("File cannot be opened")
stored_bytes_written = 0
threshold_size = 1048576 * 100 # 100 Mbyte
with open(filename, "wb") as file:
bytes_written = 0
flag_monitor = False
if debug and content_length is not None and content_length > threshold_size:
flag_monitor = True
for chunk in ret.iter_content(chunk_size=1024):
file.write(chunk)
if flag_monitor:
bytes_written += 1024
diff = bytes_written - stored_bytes_written
if diff > threshold_size:
stored_bytes_written = bytes_written
total = int(content_length/1048576)
rate = int(100.*float(bytes_written)/float(content_length))
util.log("[download_monitor] total={}MB, {}% downloaded".format(
total, rate), flush=True)
util.log("==> downloaded with file = {}".format(filename), flush=True)
if unzip:
if out_directory is None:
out_directory = "."
util.log("==> unzip file under {}".format(out_directory))
zip_file = zipfile.ZipFile(filename)
zip_file.extractall(out_directory)
zip_file.close()
if os.path.exists(filename):
util.log("==> remove file = {}".format(filename))
os.remove(filename)
[docs] def post(self, v, out_directory=None, debug=True, flag_path=False, flag_subdirectory=False, disable_hash=False):
"""
REST API: 単一ファイルダウンロード実行
* Endpoint: [agent-url]/benten/v1/download/file
* Method: POST
* Response: HTTP (JSON when error occured)
* Authorization: 要
:param \**v:
See below.
:Keyword Arguments:
* *file* (``str`` ) : ファイル名
:param debug:
True の時に debug print される (Default値はTrue)
:param out_directory:
ダウンロードファイルを出力するディレクトリ (flag_pathがTrueの時に有効になる)
:param flag_path:
Trueの時に out_directory 以下にダウンロードダウンロードファイルを出力する
:param flag_subdirectory:
Trueの時、ファイルは full pathでなく、登録ディレクトリ以下のsub directoryで保存する
:param disable_hash
Trueの時、ダウンロード後のハッシュチェックを省略する
:return:
なし
(注) Rest APIの返答においてContent-Disposition の header に記載されたファイル名で ファイルコンテンツのダウンロードを行う。
"""
endpoint = "%s/%s" % (self.__endpoint, v)
ret = requests.post(self.__endpoint, v, verify=False,
headers=util.headers_authorization(self.__parent.access_token()),stream=True)
if ret.status_code != 200:
message = "HTTP status: " + str(ret.status_code)
raise util.Error(message, domain=util.error_domain(
__file__, sys._getframe()))
content_type = ret.headers["content-type"]
try:
content_length = int(ret.headers.get("Content-Length"))
except:
content_length = None
if "json" in content_type:
return util.json_response(ret)
if "application/octet-stream" not in content_type:
message = "Content-Type: " + content_type
raise util.Error(message, domain=util.error_domain(
__file__, sys._getframe()))
try:
content_length = int(ret.headers.get("Content-Length"))
except:
content_length = None
file_hash = ret.cookies.get("file_hash")
if file_hash is not None:
file_hash = file_hash.strip('"')
file_time = ret.cookies.get("file_time")
if file_time is not None:
file_time = file_time.strip('"')
file_subdirectory = ret.cookies.get("file_subdirectory")
if file_subdirectory is not None:
file_subdirectory = file_subdirectory.strip('"')
try:
content_disposition = ret.headers["Content-Disposition"]
filename = content_disposition.split("filename=")[1].strip()
filename = urlparse.unquote(filename)
except:
raise util.Error("File cannot be opened")
file_path = filename
if file_subdirectory in ["", None]:
flag_subdirectory = False
# ,.. define dirname/file_path to output file
dirname = None
file_basename = os.path.basename(filename)
file_dirname = os.path.dirname(filename)
if flag_path in [False, None]:
if out_directory in [False, None]:
if flag_subdirectory in [False, None]:
dirname = None
else:
dirname = file_subdirectory
else:
if flag_subdirectory in [False, None]:
dirname = out_directory
else:
dirname = "{}/{}".format(out_directory, file_subdirectory)
else:
if out_directory in [False, None]:
dirname = ".{}".format(file_dirname)
else:
dirname = "{}{}".format(out_directory,file_dirname)
if dirname is None:
file_path = file_basename
else:
file_path = "{}/{}".format(dirname, file_basename)
if dirname is not None:
util.makedirs(dirname)
stored_bytes_written = 0
threshold_size = 1048576 * 100 # 100 Mbyte
with open(file_path, "wb") as file:
bytes_written = 0
flag_monitor = False
if debug and content_length is not None and content_length > threshold_size:
flag_monitor = True
for chunk in ret.iter_content(chunk_size=1024):
file.write(chunk)
if flag_monitor:
bytes_written += 1024
diff = bytes_written - stored_bytes_written
if diff > threshold_size:
stored_bytes_written = bytes_written
total = int(content_length/1048576)
rate = int(100.*(float(bytes_written)/float(content_length)))
util.log("[download_monitor] total={}MB, {}% downloaded".format(
total, rate), flush=True)
util.log("==> downloaded with file = {}".format(file_path), flush=True)
if file_time:
mtime = util.mktime(file_time)
atime = mtime
# os.utime(file_path, times=(atime, mtime)) # <- not work with python2.7
os.utime(file_path, (atime, mtime))
if file_hash and disable_hash in [None, False, 0]:
if debug:
util.log("==> check consitency with hash = {}".format(file_hash), flush=True)
hash_check = util.checksum(file_path)
if hash_check != file_hash:
try:
os.remove(file_path)
except:
pass
raise util.Error("download again (inconsistent hash in download file)")