beta site release

This commit is contained in:
Kieran 2018-11-20 01:22:14 +08:00
parent f1c5ef33d9
commit b0a49e5bc2
11 changed files with 309 additions and 121 deletions

View File

@ -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 |
---

View File

@ -37,10 +37,7 @@
<template id="tmpl-view-default">
<div class="view-default">
<div>
<b>Public Hash:</b> <span class="view-public-hash"></span>
</div>
<div>
<b>Hash:</b> <span class="view-hash"></span>
<b>Id:</b> <span class="view-file-id"></span>
</div>
<div>
<b>Key:</b> <span class="view-key"></span>
@ -51,6 +48,13 @@
<div class="btn-download">
Download
</div>
<div class="view-download-progress">
<div>0%</div>
<div></div>
</div>
<div>
<div class="view-download-label-speed"></div>
</div>
</div>
</template>
</head>

View File

@ -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;
}

View File

@ -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<XMLHttpRequest>} 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<File>} 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<CryptoKey>} 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);

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}
?>

View File

@ -1,11 +1,19 @@
<?php
class Download implements RequestHandler {
public function HandleRequest() : void {
$fs = new FileStore();
if(isset($_REQUEST["hash"])){
$hash = $_REQUEST["hash"];
private $Config;
$file_info = $fs->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){

View File

@ -1,11 +1,11 @@
<?php
class FileInfo {
public $PublicHash;
public $Hash;
public $Path;
public $FileId;
public $Views;
public $Uploaded;
public $Hash;
public $LastView;
public $Uploaded;
public $Size;
public $DownloadHost;
}
?>

View File

@ -1,44 +1,50 @@
<?php
class FileStore {
private $UploadFolder;
public function __construct($path) {
if($path !== FALSE){
$this->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');
}
}
?>

View File

@ -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;
}
}
?>