diff --git a/README.md b/README.md index 1ef18b2..2dd3358 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,14 @@ Setup * php-fpm * php-redis * php-curl + * php-gmp * Redis Nginx handler ==== ``` -location ~ "^\/([0-9a-z\.]{36,40})$" { - try_files $uri /src/php/handler.php?h=download&hash=$1; +location ~* "^\/([0-9a-z]{27})$" { + try_files $uri /src/php/handler.php?h=download&id=$1; } ``` @@ -21,7 +22,8 @@ Void Binary File Format (VBF) | Name | Type | Description | |---|---|---| | version | uint8_t | Binary file format version | -| hash | 32 byte hash | The hash of the unencrypted file | +| hash | SHA256 hash | The hash of the unencrypted file | +| uploaded | uint32_t | Timestamp of when the upload started | | payload | >EOF | The encrypted payload | --- diff --git a/index.html b/index.html index b35cf27..6093846 100644 --- a/index.html +++ b/index.html @@ -37,10 +37,7 @@ diff --git a/src/css/style.scss b/src/css/style.scss index 6453c25..fdfd77f 100644 --- a/src/css/style.scss +++ b/src/css/style.scss @@ -171,21 +171,34 @@ html, body { grid-template-columns: 60% 40%; } -.view-default div:nth-child(1), div:nth-child(2), div:nth-child(3), div:nth-child(4) { - line-height: 20px; -} - .view-default span { float: right; } .view-default div:nth-child(4) { - grid-row-start: 4; + grid-row-start: 5; +} +.view-default div:nth-child(5) { + background-color: #333; + color: rgb(214, 214, 214); } .view-default .btn-download { line-height: 100px; grid-column-start: 2; grid-row-start: 1; - grid-row-end: 5; + grid-row-end: 1; +} + +.view-default .view-download-progress div:nth-child(1) { + text-align: center; + height: 100%; + line-height: 20px; +} + +.view-default .view-download-progress div:nth-child(2) { + background-color: rgb(0, 146, 0); + height: 100%; + width: 0px; + margin-top: -20px; } \ No newline at end of file diff --git a/src/js/script.js b/src/js/script.js index 0dc5768..71beb8c 100644 --- a/src/js/script.js +++ b/src/js/script.js @@ -1,8 +1,20 @@ +/** + * @constant {string} - The hashing algo to use to verify the file + */ +const HashingAlgo = 'SHA-256'; /** * @constant {string} - The encryption algoritm to use for file uploads */ -const EncryptionAlgo = 'AES-GCM'; +const EncryptionAlgo = 'AES-CBC'; +/** + * @constant {object} - The 'algo' argument for importing/exporting/generating keys + */ +const EncryptionKeyDetails = { name: EncryptionAlgo, length: 128 }; +/** + * @constant {object} - The 'algo' argument for importing/exporting/generating hmac keys + */ +const HMACKeyDetails = { name: 'HMAC', hash: HashingAlgo }; /** * @constant {number} - Size of 1 kiB */ @@ -90,6 +102,21 @@ const App = { */ ArrayToHex: (buffer) => Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''), + /** + * Converts hex to ArrayBuffer + * @param {string} hex - The hex to parse into ArrayBuffer + * @returns {ArrayBuffer} The parsed hex data + */ + HexToArray: (hex) => { + let ret = new Uint8Array(hex.length / 2) + + for (let i = 0; i < hex.length; i += 2) { + ret[i / 2] = parseInt(hex.substring(i, i + 2), 16) + } + + return ret.buffer + }, + /** * Formats bytes into binary notation * @param {number} b - The value in bytes @@ -122,7 +149,7 @@ const App = { * Sets up the page */ Init: function () { - if(location.hash !== "") { + if (location.hash !== "") { App.Elements.PageUpload.style.display = "none"; App.Elements.PageView.style.display = "block"; new ViewManager(); @@ -153,9 +180,12 @@ const JsonXHR = async function (method, url, data) { * @param {string} url - Request URL * @param {[*]} data - Request payload (method must be post) * @param {[*]} headers - Headers to add to the request + * @param {[function]} uploadprogress - Progress function from data uploads + * @param {[function]} downloadprogress - Progress function for data downloads + * @param {[function]} editrequest - Function that can edit the request before its sent * @returns {Promise} The completed request */ -const XHR = function (method, url, data, headers, progress) { +const XHR = function (method, url, data, headers, uploadprogress, downloadprogress, editrequest) { return new Promise(function (resolve, reject) { let x = new XMLHttpRequest(); x.onreadystatechange = function (ev) { @@ -164,8 +194,13 @@ const XHR = function (method, url, data, headers, progress) { } }; x.upload.onprogress = function (ev) { - if (typeof progress === "function") { - progress(ev); + if (typeof uploadprogress === "function") { + uploadprogress(ev); + } + }; + x.onprogress = function (ev) { + if (typeof downloadprogress === "function") { + downloadprogress(ev); } }; x.onerror = function (ev) { @@ -173,6 +208,9 @@ const XHR = function (method, url, data, headers, progress) { }; x.open(method, url, true); + if (typeof editrequest === "function") { + editrequest(x); + } //set headers if they are passed if (typeof headers === "object") { for (let h in headers) { @@ -187,15 +225,18 @@ const XHR = function (method, url, data, headers, progress) { }) }; +/** + * Calls api handler + */ const Api = { - DoRequest: async function(req) { - return JSON.parse((await JsonXHR('POST', '/api', req)).response); + DoRequest: async function (req) { + return JSON.parse((await JsonXHR('POST', '/api', req)).response); }, - GetFileInfo: async function(hash) { + GetFileInfo: async function (id) { return await Api.DoRequest({ cmd: 'file_info', - hash: hash + id: id }); } }; @@ -227,23 +268,23 @@ const DropzoneManager = function (dz) { * */ const ViewManager = function () { - this.hash = null; + this.id = null; this.key = null; this.iv = null; - this.ParseUrlHash = function() { + this.ParseUrlHash = function () { let hs = window.location.hash.substr(1).split(':'); - this.hash = hs[0]; + this.id = hs[0]; this.key = hs[1]; this.iv = hs[2]; }; - this.LoadView = async function() { + this.LoadView = async function () { this.ParseUrlHash(); - let fi = await Api.GetFileInfo(this.hash); + let fi = await Api.GetFileInfo(this.id); - if(fi.ok === true){ + if (fi.ok === true) { $('#page-view .file-info-size').textContent = App.Utils.FormatBytes(fi.data.Size); $('#page-view .file-info-views').textContent = fi.data.Views.toLocaleString(); $('#page-view .file-info-last-download').textContent = new Date(fi.data.LastView * 1000).toLocaleString(); @@ -253,15 +294,33 @@ const ViewManager = function () { } }; - this.ShowPreview = async function(fileinfo) { + this.ShowPreview = async function (fileinfo) { let nelm = document.importNode($("template[id='tmpl-view-default']").content, true); - nelm.querySelector('.view-public-hash').textContent = fileinfo.PublicHash; - nelm.querySelector('.view-hash').textContent = fileinfo.Hash; + nelm.querySelector('.view-file-id').textContent = fileinfo.FileId; nelm.querySelector('.view-key').textContent = this.key; nelm.querySelector('.view-iv').textContent = this.iv; - + nelm.querySelector('.btn-download').addEventListener('click', function () { + let fd = new FileDownloader(this.fileinfo, this.self.key, this.self.iv); + fd.onprogress = function(x) { + this.elm_bar.style.width = `${100 * x}%`; + this.elm_bar_label.textContent = `${(100 * x).toFixed(0)}%`; + }.bind({ + elm_bar_label: document.querySelector('.view-download-progress div:nth-child(1)'), + elm_bar: document.querySelector('.view-download-progress div:nth-child(2)') + }); + fd.DownloadFile().then(function (file){ + var objurl = URL.createObjectURL(file); + var dl_link = document.createElement('a'); + dl_link.href = objurl; + dl_link.download = file.name; + dl_link.click(); + }); + }.bind({ + self: this, + fileinfo: fileinfo + })); $('#page-view').appendChild(nelm); - + }; this.LoadView(); @@ -274,8 +333,10 @@ const ViewManager = function () { * @param {string} key - The key to use for decryption * @param {string} iv - The IV to use for decryption */ -const FileDownloader = function(fileinfo, key, iv) { +const FileDownloader = function (fileinfo, key, iv) { this.fileinfo = fileinfo; + this.key = key; + this.iv = iv; /** * Track download stats @@ -286,12 +347,67 @@ const FileDownloader = function(fileinfo, key, iv) { lastProgress: 0 }; + this.HandleProgress = function(type, progress) { + switch(type){ + case 'progress-download':{ + if(typeof this.onprogress === 'function'){ + this.onprogress(progress); + } + } + } + }; + /** * Downloads the file * @returns {Promise} The loaded and decripted file */ - this.DownloadFile = async function() { + this.DownloadFile = async function () { + let link = (this.fileinfo.DownloadHost !== null ? `${window.location.protocol}//${this.fileinfo.DownloadHost}` : '') + `/${this.fileinfo.FileId}` + Log.I(`Starting download from: ${link}`); + let fd = await XHR('GET', link, undefined, undefined, undefined, function (ev) { + this.HandleProgress('progress-download', ev.loaded / parseFloat(ev.total)); + }.bind(this), function (req) { + req.responseType = "arraybuffer"; + }); + let blob = fd.response; + let header = VBF.Parse(blob); + let hash_text = App.Utils.ArrayToHex(header.hmac); + + Log.I(`${this.fileinfo.FileId} blob header version is ${header.version} and hash is ${hash_text} uploaded on ${header.uploaded}`); + + //attempt decryption + try { + let key_raw = App.Utils.HexToArray(this.key); + let iv_raw = App.Utils.HexToArray(this.iv); + Log.I(`${this.fileinfo.FileId} decrypting with key ${this.key} and iv ${this.iv}`); + + let key = await crypto.subtle.importKey("raw", key_raw, EncryptionKeyDetails, false, ['decrypt']); + let keyhmac = await crypto.subtle.importKey("raw", key_raw, HMACKeyDetails, false, ['verify']); + + let decrypted_file = await crypto.subtle.decrypt({ name: EncryptionAlgo, iv: iv_raw }, key, blob.slice(VBF.HeaderSize)); + + //read the header + let json_header_length = new Uint16Array(decrypted_file)[0]; + let json_header_text = new TextDecoder('utf-8').decode(decrypted_file.slice(2, json_header_length + 2)); + Log.I(`${this.fileinfo.FileId} header is ${json_header_text}`); + + //hash the file to verify + let file_data = decrypted_file.slice(2 + json_header_length); + let hmac_verify = await crypto.subtle.verify("HMAC", keyhmac, header.hmac, file_data); + if (hmac_verify) { + Log.I(`${this.fileinfo.FileId} HMAC verified!`); + + let header_obj = JSON.parse(json_header_text); + return new File([file_data], header_obj.name, { + type: header_obj.mime + }); + } else { + throw "HMAC verify failed"; + } + } catch (ex) { + Log.E(`${this.fileinfo.FileId} error decrypting file: ${ex}`); + } }; }; @@ -305,7 +421,8 @@ const FileUpload = function (file) { this.file = file; this.domNode = null; this.key = null; - this.iv = new Uint32Array(16); + this.hmackey = null; + this.iv = new Uint8Array(16); /** * Track uplaod stats @@ -358,10 +475,10 @@ const FileUpload = function (file) { fr.onload = function (ev) { this.HandleProgress('state-hash-start'); - crypto.subtle.digest("SHA-256", ev.target.result).then(function (hash) { + crypto.subtle.sign("HMAC", this.hmackey, ev.target.result).then(function (hash) { this.HandleProgress('state-hash-end'); resolve({ - h256: hash, + hash: hash, data: ev.target.result }); }.bind(this)); @@ -484,7 +601,7 @@ const FileUpload = function (file) { */ this.CreateNode = function () { let nelm = document.importNode(App.Templates.Upload.content, true); - + nelm.filename = nelm.querySelector('.file-info .file-info-name'); nelm.filesize = nelm.querySelector('.file-info .file-info-size'); nelm.filespeed = nelm.querySelector('.file-info .file-info-speed'); @@ -507,7 +624,9 @@ const FileUpload = function (file) { * @returns {Promise} The new key */ this.GenerateKey = async function () { - this.key = await crypto.subtle.generateKey({ name: EncryptionAlgo, length: 128 }, true, ['encrypt', 'decrypt']); + this.key = await crypto.subtle.generateKey(EncryptionKeyDetails, true, ['encrypt', 'decrypt']); + this.hmackey = await crypto.subtle.importKey("raw", await crypto.subtle.exportKey('raw', this.key), HMACKeyDetails, false, ["sign"]); + crypto.getRandomValues(this.iv); this.domNode.key.textContent = `Key: ${await this.TextKey()}`; @@ -557,7 +676,7 @@ const FileUpload = function (file) { * Creates a header object to be prepended to the file for encrypting * @returns {any} */ - this.CreateHeader = function() { + this.CreateHeader = function () { return { name: this.file.name, mime: this.file.type, @@ -576,47 +695,38 @@ const FileUpload = function (file) { await this.GenerateKey(); let header = JSON.stringify(this.CreateHeader()); let hash_data = await this.HashFile(); - let h256 = App.Utils.ArrayToHex(hash_data.h256); + let h256 = App.Utils.ArrayToHex(hash_data.hash); Log.I(`${this.file.name} hash is: ${h256}`); - //check file params are ok - //TODO: call to api to check file info - //create blob for encryption - Log.I(`Using header: ${header}`); let header_data = new TextEncoder().encode(header); - + Log.I(`Using header: ${header} (length=${header_data.byteLength})`); + let encryption_payload = new Uint8Array(2 + header_data.byteLength + hash_data.data.byteLength); let header_length_data = new Uint16Array(1); header_length_data[0] = header_data.byteLength; //header length encryption_payload.set(header_length_data, 0); encryption_payload.set(new Uint8Array(header_data), 2); //the file info header - encryption_payload.set(new Uint8Array(hash_data.data), 2 + header_data.byteLength); - + encryption_payload.set(new Uint8Array(hash_data.data), 2 + header_data.byteLength); + //encrypt with the key Log.I(`Encrypting ${this.file.name} with key ${await this.HexKey()} and IV ${this.HexIV()}`) let encryptedData = await this.EncryptFile(encryption_payload); - //upload the encrypted file data Log.I(`Uploading file ${this.file.name}`); - let upload_payload = new Uint8Array(1 + hash_data.h256.byteLength + encryptedData.byteLength); - - upload_payload[0] = 1; //blob version - upload_payload.set(new Uint8Array(hash_data.h256), 1); - upload_payload.set(new Uint8Array(encryptedData), 1 + hash_data.h256.byteLength); - + let upload_payload = VBF.Create(hash_data.hash, encryptedData); let uploadResult = await this.UploadData(upload_payload); Log.I(`Got response for file ${this.file.name}: ${JSON.stringify(uploadResult)}`); this.domNode.state.parentNode.style.display = "none"; this.domNode.progress.parentNode.style.display = "none"; - + if (uploadResult.status === 200) { this.domNode.links.style.display = ""; let nl = document.createElement("a"); nl.target = "_blank"; - nl.href = `${window.location.protocol}//${window.location.host}/#${uploadResult.pub_hash}:${await this.TextKey()}`; + nl.href = `${window.location.protocol}//${window.location.host}/#${uploadResult.id}:${await this.TextKey()}`; nl.textContent = this.file.name; this.domNode.links.appendChild(nl); } else { @@ -625,4 +735,42 @@ const FileUpload = function (file) { } }; }; + +const VBF = { + Version: 1, + HeaderSize: 37, + + Create: function(hash, encryptedData) { + //upload the encrypted file data + let upload_payload = new Uint8Array(VBF.HeaderSize + encryptedData.byteLength); + + let created = new ArrayBuffer(4); + new DataView(created).setUint32(0, parseInt(new Date().getTime() / 1000), true); + + upload_payload[0] = VBF.Version; //blob version + upload_payload.set(new Uint8Array(hash), 1); + upload_payload.set(new Uint8Array(created), hash.byteLength + 1); + upload_payload.set(new Uint8Array(encryptedData), VBF.HeaderSize); + + return upload_payload; + }, + + /** + * Parses the header of the raw file + * @param {ArrayBuffer} data - Raw data from the server + * @returns {*} The header + */ + Parse: function(data) { + let version = new Uint8Array(data)[0]; + let hmac = data.slice(1, 33); + let uploaded = new DataView(data.slice(33, 37)).getUint32(0, true); + + return { + version, + hmac, + uploaded + }; + } +}; + setTimeout(App.Init); diff --git a/src/php/api.php b/src/php/api.php index 1d5292d..e0c4cc6 100644 --- a/src/php/api.php +++ b/src/php/api.php @@ -7,6 +7,8 @@ } class Api implements RequestHandler { + private $Config; + public function __construct(){ ini_set('enable_post_data_reading', 0); } @@ -14,15 +16,17 @@ public function HandleRequest() : void { $cmd = json_decode(file_get_contents("php://input")); + $this->Config = Config::MGetConfig(array('upload_folder')); + $rsp = new ApiResponse(); $rsp->cmd = $cmd; - $fs = new FileStore(); + $fs = new FileStore($this->Config->upload_folder); switch($cmd->cmd){ case "file_info":{ $rsp->ok = true; - $rsp->data = $fs->GetPublicFileInfo($cmd->hash); + $rsp->data = $fs->GetPublicFileInfo($cmd->id); break; } } diff --git a/src/php/blobfile.php b/src/php/blobfile.php index 5dad753..df0f975 100644 --- a/src/php/blobfile.php +++ b/src/php/blobfile.php @@ -2,16 +2,19 @@ class BlobFile { public $Version; public $Hash; + public $Uploaded; + + public static function LoadHeader() : ?BlobFile { + $input = fopen("php://input", "rb"); + $header = fread($input, 37); //1 version byte + 32 byte hash (64 hex digits) + 4 byte timestamp + fclose($input); - public static function LoadHeader($stream) : ?BlobFile { - $header = fread($stream, 33); //1 version byte + 32 byte hash (64 hex digits) - rewind($stream); - - $header_data = unpack("C1version/H64hash256", $header); + $header_data = unpack("C1version/H64hash256/Vuploaded", $header); if($header_data["version"] == 1){ $bf = new BlobFile(); $bf->Version = $header_data["version"]; $bf->Hash = $header_data["hash256"]; + $bf->Uploaded = $header_data["uploaded"]; return $bf; } diff --git a/src/php/config.php b/src/php/config.php index c0cf9ed..b74e325 100644 --- a/src/php/config.php +++ b/src/php/config.php @@ -7,7 +7,7 @@ public static function MGetConfig($config_name) { $redis = StaticRedis::$Instance; - return $redis->hMGet(REDIS_PREFIX . 'config', $config_name); + return (object)$redis->hMGet(REDIS_PREFIX . 'config', $config_name); } } ?> \ No newline at end of file diff --git a/src/php/download.php b/src/php/download.php index ec30fa7..f65a039 100644 --- a/src/php/download.php +++ b/src/php/download.php @@ -1,11 +1,19 @@ GetPublicFileInfo($hash); + public function HandleRequest() : void { + $this->Config = Config::MGetConfig(array('upload_folder')); + + if($this->Config->upload_folder == FALSE){ + $this->Config->upload_folder = Upload::$UploadFolderDefault; + } + + $fs = new FileStore($this->Config->upload_folder); + if(isset($_REQUEST["id"])){ + $id = $_REQUEST["id"]; + + $file_info = $fs->GetPublicFileInfo($id); if($file_info != NULL){ $this->StartDownload($file_info); } else { @@ -23,7 +31,7 @@ $tracking = new Tracking(); //pass to nginx to handle download - $this->InternalNginxRedirect($file_info->Path, 604800); + $this->InternalNginxRedirect($this->Config->upload_folder . '/' . $file_info->FileId, 604800); } function InternalNginxRedirect($location, $expire){ diff --git a/src/php/fileinfo.php b/src/php/fileinfo.php index be3065f..30561a9 100644 --- a/src/php/fileinfo.php +++ b/src/php/fileinfo.php @@ -1,11 +1,11 @@ \ No newline at end of file diff --git a/src/php/filestore.php b/src/php/filestore.php index e1fb7bf..91fc95a 100644 --- a/src/php/filestore.php +++ b/src/php/filestore.php @@ -1,44 +1,50 @@ UploadFolder = $path; + } else { + $this->UploadFolder = Upload::$UploadFolderDefault; + } + } + public function SetPublicFileInfo($info) : void { $redis = StaticRedis::$Instance; - $file_key = REDIS_PREFIX . $info->PublicHash; + $file_key = REDIS_PREFIX . $info->FileId; $redis->hMSet($file_key, array( - 'path' => $info->Path, 'views' => $info->Views, - 'uploaded' => $info->Uploaded, - 'lastview' => $info->LastView, - 'size' => $info->Size, - 'hash' => $info->Hash + 'lastview' => $info->LastView )); } - public function GetPublicFileInfo($public_hash) : ?FileInfo { + public function GetPublicFileInfo($id) : ?FileInfo { $redis = StaticRedis::$Instance; - $file_key = REDIS_PREFIX . $public_hash; + $file_key = REDIS_PREFIX . $id; + + $public_file_info = $redis->hMGet($file_key, array('views', 'lastview')); + if($public_file_info['views'] != False){ + $file_stat = stat("$_SERVER[DOCUMENT_ROOT]/$this->UploadFolder/$id"); - $public_file_info = $redis->hMGet($file_key, array('path', 'hash', 'views', 'uploaded', 'lastview', 'size')); - if($public_file_info['path'] != False){ $file = new FileInfo(); - $file->PublicHash = $public_hash; - $file->Hash = $public_file_info['hash']; - $file->Path = $public_file_info['path']; + $file->FileId = $id; $file->Views = intval($public_file_info['views']); - $file->Uploaded = intval($public_file_info['uploaded']); $file->LastView = intval($public_file_info['lastview']); - $file->Size = intval($public_file_info['size']); - + $file->Size = $file_stat["size"]; + $file->Uploaded = $file_stat["ctime"]; + return $file; } return NULL; } - public function FileExists($public_hash) : Boolean { + public function FileExists($id) : Boolean { $redis = StaticRedis::$Instance; - $file_key = REDIS_PREFIX . $public_hash; - return $redis->hExists($file_key, 'path'); + $file_key = REDIS_PREFIX . $id; + return $redis->hExists($file_key, 'views'); } } ?> \ No newline at end of file diff --git a/src/php/upload.php b/src/php/upload.php index 6ab00a8..c32219a 100644 --- a/src/php/upload.php +++ b/src/php/upload.php @@ -2,32 +2,33 @@ class UploadResponse { public $status = 0; public $msg; - public $pub_hash; + public $id; } class Upload implements RequestHandler { public static $UploadFolderDefault = "out"; + private $Config; private $isMultipart = False; private $MaxUploadSize = 104857600; //100MiB is the default upload size private $UploadFolder = NULL; private $PublicHashAlgo = "ripemd160"; - public function __construct(){ - $cfg = Config::MGetConfig(array('max_size', 'upload_folder', 'public_hash_algo')); + public function __construct() { + $this->Config = Config::MGetConfig(array('max_size', 'upload_folder', 'public_hash_algo')); - if($cfg["max_size"] != False){ - $this->MaxUploadSize = $cfg["max_size"]; + if($this->Config->max_size !== False){ + $this->MaxUploadSize = $this->Config->max_size; } - if($cfg["upload_folder"] != False){ - $this->UploadFolder = $cfg["upload_folder"]; + if($this->Config->upload_folder !== False){ + $this->UploadFolder = $this->Config->upload_folder; } else { $this->UploadFolder = self::$UploadFolderDefault; } - if($cfg["public_hash_algo"] != False){ - $this->PublicHashAlgo = $cfg["public_hash_algo"]; + if($this->Config->public_hash_algo !== False){ + $this->PublicHashAlgo = $this->Config->public_hash_algo; } //set php params @@ -51,21 +52,17 @@ $rsp->status = 1; $rsp->msg = "File is too large"; } else { - $input = fopen("php://input", "rb"); - $bf = BlobFile::LoadHeader($input); + $bf = BlobFile::LoadHeader(); if($bf != null){ - //generate public hash - $pub_hash = hash($this->PublicHashAlgo, $bf->Hash); - //save upload - $this->SaveUpload($input, $bf->Hash, $pub_hash); + $id = $this->SaveUpload($bf); //sync to other servers - $this->SyncFileUpload($input); + $this->SyncFileUpload($id); $rsp->status = 200; - $rsp->pub_hash = $pub_hash; + $rsp->id = $id; } else { $rsp->status = 2; $rsp->msg = "Invalid file header"; @@ -75,27 +72,30 @@ echo json_encode($rsp); } - function SyncFileUpload() { + function SyncFileUpload($id) : void { } - function SaveUpload($input, $hash, $pub_hash) { - $fs = new FileStore(); - $file_path = "$this->UploadFolder/$pub_hash"; + function SaveUpload($bf) : string { + $id = gmp_strval(gmp_init("0x" . hash($this->PublicHashAlgo, $bf->Hash)), 62); + + $fs = new FileStore($this->UploadFolder); + $file_path = "$this->UploadFolder/$id"; $fi = new FileInfo(); - $fi->PublicHash = $pub_hash; - $fi->Hash = $hash; - $fi->Path = $file_path; - $fi->Uploaded = time(); + $fi->FileId = $id; $fi->LastView = time(); - $fi->Views = 0; + $fi->Views = 1; + $input = fopen("php://input", "rb"); $fout = fopen("$_SERVER[DOCUMENT_ROOT]/$file_path", 'wb+'); $fi->Size = stream_copy_to_stream($input, $fout); fclose($fout); + fclose($input); $fs->SetPublicFileInfo($fi); + + return $id; } } ?> \ No newline at end of file