#!/usr/bin/lua

local ltn12 = require("luci.ltn12")
local sys = require("luci.sys")
local fs = require("luci.fs")
local sauth = require("luci.sauth")
local log = require("xiaoqiang.XQLog")
local XQConfigs = require("xiaoqiang.common.XQConfigs")

function httpWrite(body)
    io.write("Status: 200 OK\r\n")
    io.write("\r\n")
    io.write(tostring(body or ""))
    io.flush()
    io.close()
end

function uploadSucceedResponse(savename)
    local LuciJson = require("cjson")
    local body = {}
    body["code"] = 0
    body["savename"] = savename
    body["msg"] = ""
    body = LuciJson.encode(body)
    httpWrite(body)
end

function tunnelRequestDatacenter(payload)
    local LuciJson = require("cjson")
    local LuciUtil = require("luci.util")
    local XQCryptoUtil = require("xiaoqiang.util.XQCryptoUtil")
    payload = LuciJson.encode(payload)
    payload = XQCryptoUtil.binaryBase64Enc(payload)
    local cmd = XQConfigs.THRIFT_TUNNEL_TO_DATACENTER % payload
    local result = LuciUtil.exec(cmd)
    httpWrite(result)
end

function backupUploadFile(path, modifytime, filename, backuptype, id, relativepath, devicename, backupcount)
    local payload = {}
    payload["api"] = 53
    payload["path"] = path
    payload["modifyTime"] = modifytime
    payload["filename"] = filename
    payload["backupType"] = backuptype
    payload["id"] = id
    payload["relativePath"] = relativepath
    payload["deviceName"] = devicename
    payload["backupCount"] = backupcount
    tunnelRequestDatacenter(payload)
end

function setThumbnailPathToDB(hash, thumbnailPath)
    local payload = {}
	payload["api"] = 68
	payload["hash"] = hash
	payload["thumbnailPath"] = thumbnailPath
	tunnelRequestDatacenter(payload)
end

function setTransferedFileToDB(from, to, path, name)
    local payload = {}
    payload["api"] = 88
    payload["devicefrom"] = from
    payload["deviceto"] = to
    payload["realpath"] = path
    payload["filename"] = name
    tunnelRequestDatacenter(payload)
end

function limitsource(handle, limit)
    limit = limit or 0
    local BLOCKSIZE = ltn12.BLOCKSIZE

    return function()
        if limit < 1 then
            handle:close()
            return nil
        else
            local read = (limit > BLOCKSIZE) and BLOCKSIZE or limit
            limit = limit - read

            local chunk = handle:read(read)
            if not chunk then handle:close() end
            return chunk
        end
    end
end

function urldecode_params(url, tbl)
    local params = tbl or { }

    if url:find("?") then
        url = url:gsub("^.+%?([^?]+)", "%1")
    end

    for pair in url:gmatch("[^&;]+") do

        -- find key and value
        local key = urldecode(pair:match("^([^=]+)"))
        local val = urldecode(pair:match("^[^=]+=(.+)$"))

        -- store
        if type(key) == "string" and key:len() > 0 then
            if type(val) ~= "string" then val = "" end

            if not params[key] then
                params[key] = val
            elseif type(params[key]) ~= "table" then
                params[key] = {params[key], val}
            else
                table.insert(params[key], val)
            end
        end
    end

    return params
end

function urldecode(str, no_plus)
    local function __chrdec(hex)
        return string.char(tonumber(hex, 16))
    end

    if type(str) == "string" then
        if not no_plus then
            str = str:gsub("+", " ")
        end

        str = str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", __chrdec)
    end

    return str
end

function initval(tbl, key)
    if tbl[key] == nil then
        tbl[key] = ""
    elseif type(tbl[key]) == "string" then
        --tbl[key] = {tbl[key], ""}
        tbl[key] = ""
    else
        table.insert(tbl[key], "")
    end
end

function appendval(tbl, key, chunk)
    if type(tbl[key]) == "table" then
        tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
    else
        tbl[key] = tbl[key] .. chunk
    end
end

function parse_message_body(src, msg)
    if msg and msg.env.CONTENT_TYPE then
        msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
    end

    if not msg.mime_boundary then
        return nil, "Invalid Content-Type found"
    end

    local tlen   = 0
    local inhdr  = false
    local field  = nil
    local store  = nil
    local lchunk = nil

    local function parse_headers(chunk, field)
        local stat
        repeat
            chunk, stat = chunk:gsub(
                "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
                function(k,v)
                    field.headers[k] = v
                    return ""
                end
            )
        until stat == 0

        chunk, stat = chunk:gsub("^\r\n", "")

        -- End of headers
        if stat > 0 then
            if field.headers["Content-Disposition"] then
                if field.headers["Content-Disposition"]:match("^form%-data; ") then
                    field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
                end
            end

            if field.name then
                initval(msg.params, field.name)

                store = function(hdr, buf, eof)
                    appendval(msg.params, field.name, buf)
                end
            else
                store = nil
            end

            return chunk, true
        end

        return chunk, false
    end

    local function snk(chunk)
        tlen = tlen + (chunk and #chunk or 0)

        if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
            return nil, "Message body size exceeds Content-Length"
        end

        if chunk and not lchunk then
            lchunk = "\r\n" .. chunk
        elseif lchunk then
            local data = lchunk .. (chunk or "")
            local spos, epos, found

            repeat
                spos, epos = data:find("\r\n--" .. msg.mime_boundary .. "\r\n", 1, true)

                if not spos then
                    spos, epos = data:find("\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true)
                end

                if spos then
                    local predata = data:sub(1, spos - 1)

                    if inhdr then
                        predata, eof = parse_headers(predata, field)

                        if not eof then
                            return nil, "Invalid MIME section header"
                        elseif not field.name then
                            return nil, "Invalid Content-Disposition header"
                        end
                    end

                    if store then
                        store(field, predata, true)
                    end

                    field = {headers = { }}
                    found = found or true

                    data, eof = parse_headers(data:sub(epos + 1, #data), field)
                    inhdr = not eof
                end
            until not spos

            if found then
                -- We found at least some boundary
                lchunk, data = data, nil
            else
                if inhdr then
                    lchunk, eof = parse_headers(data, field)
                    inhdr = not eof
                else
                    store(field, lchunk, false)
                    lchunk, chunk = chunk, nil
                end
            end
        end

        return true
    end

    return ltn12.pump.all(src, snk)
end

function status(code, message, filepath)
    if code and code ~= 200 and filepath then
        log.log(3, "=============Error! unlink source file: " .. filepath)
        fs.unlink(filepath)
    end

    code = code or 200
    message = message or "OK"
    statusline = "Status: " .. code .. " " .. message .. " \r\n\r\n"
    print(statusline)
end

function isMounted(path)
    if path and type(path) == "string" and #path > 0 then
        local result
        local cmd = "mount | awk '{print $3}'"
        local p = io.popen(cmd)
        if p then
            result = p:read("*a")
            p:close()
        end

        if result then
            local t = {}
            for item in string.gmatch(result .. "\n", "([^\n]*)\n") do
                if item and #item > 0 then
                    table.insert(t, item)
                end
            end

            for key, value in ipairs(t) do
                if path == value then
                    return true
                end
            end
        end
    end

    return false
end

-------------------------------------------------------------------------------
-----------------------start parse message body from fcgi----------------------
-------------------------------------------------------------------------------
local env = sys.getenv()
local input = limitsource(io.stdin, tonumber(sys.getenv("CONTENT_LENGTH")))
local message = {
    env = env,
    headers = {},
    params = urldecode_params(env.QUERY_STRING or ""),
}

local conflictPrefix = "nginx_file"
for key, value in pairs(message.params) do
    if key and (string.sub(key, 1, string.len(conflictPrefix)) == conflictPrefix) then
        log.log(3, "=============conflict param=", key)
        status(400, "Bad Request")
        return
    end
end

parse_message_body(input, message)

-- get nginx module store file path
local filepath = message.params["nginx_file_path"]
if not filepath then
    status(500, "Internal Server Error")
    return
end
log.log(7, "=============filepath=", filepath)

-- get file name
local filename = message.params["nginx_file_name"]
if not filename then
    status(500, "Internal Server Error", filepath)
    return
end
filename = string.gsub(filename, "+", " ")
filename = string.gsub(filename, "%%(%x%x)",
    function(h)
        return string.char(tonumber(h, 16))
    end)
filename = filename.gsub(filename, "\r\n", "\n")
log.log(7, "=============filename=", filename)

-- get target path
local path = message.params["target"]
if path == nil then
    path = ""
else
    if string.match(path, "\/$") == nil then
        path = path .. "/"
    end
end

-- validate session
local stok = message.params["stok"]
if not stok then
    status(401, "Unauthorized", filepath)
    return
end
log.log(7, "=============stok=", stok)
local sdat = sauth.read(stok)
if not sdat then
    status(401, "Unauthorized", filepath)
    return
end

--get isfiletransfer
local isfiletransfer = message.params["is_filetransfer"]
if isfiletransfer ~= nil and isfiletransfer == "1" then
    log.log(7, "it is filetransfer action")
    local devicefrom = message.params["from"]
    if devicefrom == nil then
        devicefrom = ""
    end
    
    local deviceto = message.params["to"]
    if deviceto == nil then
        deviceto = ""
    end
    
    local path1 = filepath
    if path1 == nil then
        path1 = ""
    end
    
    local name1 = filename
    if name1 == nil then
        name1 = ""
    end
    
    setTransferedFileToDB(devicefrom, deviceto, path1, name1)
    return
end

-- get backupType
local backuptype = message.params["backupType"]
if backuptype == nil then
    backuptype = ""
end

if string.find(path, "%.%.%/") ~= nil then
    status(400, "Bad Request", filepath)
    return
end

local constPrefix1 = "/userdisk/data/"
local constPrefix2 = "/extdisks/"
local constPrefix3 = "/userdisk/privacyData/"
if (string.sub(path, 1, string.len(constPrefix1)) ~= constPrefix1) and (string.sub(path, 1, string.len(constPrefix2)) ~= constPrefix2) and (string.sub(path, 1, string.len(constPrefix3)) ~= constPrefix3) and (backuptype == "") then
    status(403, "Forbidden", filepath)
    return
end
if (string.sub(path, 1, string.len(constPrefix2)) == constPrefix2) and (backuptype == "") then
    local legal = false;
    local index = string.find(path, "/", #constPrefix2 + 1)
    if index then
        local rootPath = string.sub(path, 1, index - 1)
        log.log(7, "=============rootPath = " .. rootPath)
        if isMounted(rootPath) == true then
            legal = true
        end
    end
    if not legal then
        status(403, "Forbidden", filepath)
        return
    end
end

-- get file rename
local filerename = message.params["file_rename"]
if filerename then
    log.log(7, "=============filerename = " .. filerename)
end

-- check privacy disk permission by md5 device mac address
--[[if string.sub(path, 1, string.len(constPrefix3)) == constPrefix3 then
    local secret = message.params["secret"]
    if not secret then
        status(403, "Forbidden", filepath)
        return
    end

    log.log(7, "=============secret = " .. secret)

    local access = false
    local XQCryptoUtil = require("xiaoqiang.util.XQCryptoUtil")
    local XQDeviceUtil = require("xiaoqiang.util.XQDeviceUtil")
    local macfilterInfoList = XQDeviceUtil.getMacfilterInfoList()
    for _,value in ipairs(macfilterInfoList) do
        if value.mac ~= nil and value.mac ~= "" then
            log.log(7, "=============mac = " .. value.mac)
            if string.lower(XQCryptoUtil.md5Str(string.lower(value.mac))) == string.lower(secret) then
                log.log(7, "=============device found")
                if value.pridisk then
                    access = true
                end
                break
            end
        end
    end
    if not access then
        status(403, "Forbidden", filepath)
        return
    end
end]]

-- handle backup file
if backuptype ~= "" then
    if backuptype ~= "thumbnail" then
        local modifytime = message.params["modifyTime"]
        if modifytime == nil then
            modifytime = ""
        end 

        local id = message.params["id"]
        if id == nil then
            id = ""
        end

        local relativepath = message.params["relativePath"]
        if relativepath == nil then
            relativepath = ""
        end

        local devicename = message.params["deviceName"]
        if devicename == nil then
            devicename = ""
        end

        local backupcount = message.params["backupCount"]
        if backupcount == nil then
            backupcount = ""
        end

        backupUploadFile(filepath, modifytime, filename, backuptype, id, relativepath, devicename, backupcount)
        return
    end

    if backuptype == "thumbnail" then
        path = "/userdisk/data/.systemconfig/.thumbnails/smallVideo/"
    end
end

log.log(7, "=============path=", path)
if not fs.mkdir(path, true) then
    status(403, "Forbidden", filepath)
    return
end

-- real name
local savename
if filerename then
    savename = filerename
else
    savename = filename
    if fs.isfile(path .. savename) then
        local basename = savename
        local index = basename:match(".+()%.%w+$")
        if index then
            basename = basename:sub(1, index - 1)
        end
        local extension = savename:match(".+%.(%w+)$")
        for i = 1, 100, 1 do
            local tmpname = basename .. "(" .. i .. ")"
            if extension then
                tmpname = tmpname .. "." .. extension
            end
            if not fs.isfile(path .. tmpname) then
                savename = tmpname
                break
            end
        end
    end
end

-- move tmp file to target
local dest = path .. savename
log.log(7, "=============dest=" .. dest)
local stat = fs.rename(filepath, dest)
if stat ~= true then
    status(403, "Forbidden", filepath)
    return
end

-- handle backup thumbnail
if backuptype == "thumbnail" then
    local hash = message.params["hash"]
    if hash == nil then
        hash = ""
    end

    setThumbnailPathToDB(hash, dest)
    return
end

uploadSucceedResponse(savename)
