# 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)")