实现文件保险箱的端到端加密
*未完全测试 *不保证数据安全性
This commit is contained in:
parent
555a65ae79
commit
f83c7ed325
@ -68,13 +68,15 @@
|
|||||||
|
|
||||||
容量限制
|
容量限制
|
||||||
|
|
||||||
|
文件保险箱(端到端加密)(未完全测试)(不保证可靠性)
|
||||||
|
|
||||||
计划实现的功能
|
计划实现的功能
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
文件保险箱(端到端加密)
|
|
||||||
|
|
||||||
passwordless
|
passwordless
|
||||||
|
|
||||||
|
文件搜素
|
||||||
|
|
||||||
和管理员相关的功能
|
和管理员相关的功能
|
||||||
|
|
||||||
忘记密码
|
忘记密码
|
||||||
|
@ -7,9 +7,11 @@
|
|||||||
namespace app\controllers;
|
namespace app\controllers;
|
||||||
|
|
||||||
use app\models\UploadForm;
|
use app\models\UploadForm;
|
||||||
|
use app\models\User;
|
||||||
use app\utils\FileSizeHelper;
|
use app\utils\FileSizeHelper;
|
||||||
use app\utils\FileTypeDetector;
|
use app\utils\FileTypeDetector;
|
||||||
use Yii;
|
use Yii;
|
||||||
|
use yii\base\Exception;
|
||||||
use yii\filters\AccessControl;
|
use yii\filters\AccessControl;
|
||||||
use yii\filters\VerbFilter;
|
use yii\filters\VerbFilter;
|
||||||
use yii\web\Controller;
|
use yii\web\Controller;
|
||||||
@ -31,7 +33,7 @@ class VaultController extends Controller
|
|||||||
'rules' => [
|
'rules' => [
|
||||||
[
|
[
|
||||||
'allow' => true,
|
'allow' => true,
|
||||||
'actions' => ['index', 'download', 'delete', 'upload'],
|
'actions' => ['index', 'download', 'delete', 'upload', 'init', 'auth'],
|
||||||
'roles' => ['user'],
|
'roles' => ['user'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -43,6 +45,8 @@ class VaultController extends Controller
|
|||||||
'download' => ['GET'],
|
'download' => ['GET'],
|
||||||
'delete' => ['POST'],
|
'delete' => ['POST'],
|
||||||
'upload' => ['POST'],
|
'upload' => ['POST'],
|
||||||
|
'init' => ['POST'],
|
||||||
|
'auth' => ['POST'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@ -57,11 +61,15 @@ class VaultController extends Controller
|
|||||||
public function actionIndex($directory = null): Response|string
|
public function actionIndex($directory = null): Response|string
|
||||||
{
|
{
|
||||||
$model = Yii::$app->user->identity;
|
$model = Yii::$app->user->identity;
|
||||||
if ($model->vault_secret === null) {
|
if (empty($model->vault_secret)) {
|
||||||
return $this->render('_init',[
|
return $this->render('_init', [
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
]);
|
]);
|
||||||
}//TODO
|
} elseif (Yii::$app->session->get('vault_auth') !== true) {
|
||||||
|
return $this->render('_gateway', [
|
||||||
|
'model' => new User(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
$rootDataDirectory = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id . '.secret';
|
$rootDataDirectory = Yii::getAlias(Yii::$app->params['dataDirectory']) . '/' . Yii::$app->user->id . '.secret';
|
||||||
|
|
||||||
if ($directory === '.' || $directory == null) {
|
if ($directory === '.' || $directory == null) {
|
||||||
@ -244,4 +252,45 @@ class VaultController extends Controller
|
|||||||
return Yii::$app->response->statusCode = 200;
|
return Yii::$app->response->statusCode = 200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化文件保险箱密码
|
||||||
|
* @return Response
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function actionInit(): Response
|
||||||
|
{
|
||||||
|
$model = Yii::$app->user->identity; // 获取当前用户模型
|
||||||
|
if ($model->load(Yii::$app->request->post()) && $model->validate() && !empty($model->input_vault_secret)) {
|
||||||
|
$model->vault_secret = Yii::$app->getSecurity()->generatePasswordHash($model->input_vault_secret);
|
||||||
|
if ($model->save(false)) { // 保存用户模型
|
||||||
|
Yii::$app->session->setFlash('success', '保险箱初始化成功,请牢记密码,否则无法恢复保险箱内文件');
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '保险箱初始化失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '设定的密码未通过验证');
|
||||||
|
}
|
||||||
|
return $this->redirect('index.php?r=vault%2Findex'); // render里面好像只能这么写了
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function actionAuth(): Response
|
||||||
|
{
|
||||||
|
$user = Yii::$app->user->identity;
|
||||||
|
$model = new User();
|
||||||
|
if ($model->load(Yii::$app->request->post()) && !empty($model->vault_secret)) {
|
||||||
|
if (Yii::$app->getSecurity()->validatePassword($model->vault_secret, $user->vault_secret)) {
|
||||||
|
Yii::$app->session->set('vault_auth', true);
|
||||||
|
Yii::$app->session->setFlash('success', '文件保险箱密码验证成功');
|
||||||
|
return $this->redirect('index.php?r=vault%2Findex');
|
||||||
|
}
|
||||||
|
Yii::$app->session->setFlash('error', '保险箱密码错误');
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash('error', '密码未通过验证');
|
||||||
|
}
|
||||||
|
return $this->redirect('index.php?r=vault%2Findex');
|
||||||
|
}
|
||||||
}
|
}
|
@ -64,7 +64,8 @@ class User extends ActiveRecord implements IdentityInterface
|
|||||||
[['status', 'is_encryption_enabled', 'is_otp_enabled', 'dark_mode'], 'integer'],
|
[['status', 'is_encryption_enabled', 'is_otp_enabled', 'dark_mode'], 'integer'],
|
||||||
[['created_at', 'last_login'], 'safe'],
|
[['created_at', 'last_login'], 'safe'],
|
||||||
[['bio', 'totp_input', 'recoveryCode_input', 'name'], 'string'],
|
[['bio', 'totp_input', 'recoveryCode_input', 'name'], 'string'],
|
||||||
[['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret', 'input_vault_secret'], 'string', 'max' => 255],
|
['input_vault_secret', 'string', 'min' => 6, 'max' => 24],
|
||||||
|
[['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret'], 'string', 'max' => 255],
|
||||||
[['last_login_ip'], 'string', 'max' => 45],
|
[['last_login_ip'], 'string', 'max' => 45],
|
||||||
[['username', 'password'], 'required', 'on' => 'login'],
|
[['username', 'password'], 'required', 'on' => 'login'],
|
||||||
[['username', 'password', 'email', 'password2'], 'required', 'on' => 'register'],
|
[['username', 'password', 'email', 'password2'], 'required', 'on' => 'register'],
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use yii\bootstrap5\Html;
|
use yii\bootstrap5\Html;
|
||||||
use yii\bootstrap5\ActiveForm;
|
use yii\bootstrap5\ActiveForm;
|
||||||
|
use yii\web\View;
|
||||||
|
|
||||||
/** @var yii\web\View $this */
|
/** @var yii\web\View $this */
|
||||||
/** @var app\models\User $model */
|
/** @var app\models\User $model */
|
||||||
@ -10,19 +11,22 @@ use yii\bootstrap5\ActiveForm;
|
|||||||
$this->title = '解锁文件保险箱';
|
$this->title = '解锁文件保险箱';
|
||||||
$this->params['breadcrumbs'][] = $this->title;
|
$this->params['breadcrumbs'][] = $this->title;
|
||||||
?>
|
?>
|
||||||
<div class="vault-gateway">
|
<div class="vault-gateway">
|
||||||
<h1><?= Html::encode($this->title) ?></h1>
|
<h1><?= Html::encode($this->title) ?></h1>
|
||||||
|
|
||||||
<p>要访问文件保险箱,你必须要提供正确的保险箱密码</p>
|
<p>要访问文件保险箱,你必须要提供正确的保险箱密码</p>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-5">
|
<div class="col-lg-5">
|
||||||
<?php $form = ActiveForm::begin(); ?>
|
<?php $form = ActiveForm::begin(['id' => 'gateway-vault-form', 'action' => ['vault/auth'], 'method' => 'post']); ?>
|
||||||
<?= $form->field($model, 'vault_secret')->passwordInput()->label('保险箱密码(不是登陆密码)') ?>
|
<?= $form->field($model, 'vault_secret')->passwordInput()->label('保险箱密码(不是登陆密码)') ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<?= Html::submitButton('确认', ['class' => 'btn btn-primary']) ?>
|
<?= Html::submitButton('确认', ['class' => 'btn btn-primary']) ?>
|
||||||
|
</div>
|
||||||
|
<?php ActiveForm::end(); ?>
|
||||||
</div>
|
</div>
|
||||||
<?php ActiveForm::end(); ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php
|
||||||
|
$this->registerJsFile('@web/js/vault_gateway_hook.js', ['position' => View::POS_END]);
|
||||||
|
?>
|
@ -17,7 +17,7 @@ $this->params['breadcrumbs'][] = $this->title;
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-5">
|
<div class="col-lg-5">
|
||||||
<?php $form = ActiveForm::begin(); ?>
|
<?php $form = ActiveForm::begin(['id' => 'init-vault-form', 'action' => ['vault/init'], 'method' => 'post']); ?>
|
||||||
<?= $form->field($model, 'input_vault_secret')->label('保险箱密码(建议不要与登陆密码相同)')->passwordInput(['autofocus' => true]) ?>
|
<?= $form->field($model, 'input_vault_secret')->label('保险箱密码(建议不要与登陆密码相同)')->passwordInput(['autofocus' => true]) ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<?= Html::submitButton('初始化保险箱', ['class' => 'btn btn-primary']) ?>
|
<?= Html::submitButton('初始化保险箱', ['class' => 'btn btn-primary']) ?>
|
||||||
|
@ -120,7 +120,10 @@ $this->registerCssFile('@web/css/home_style.css');
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?= Html::tag('i', '', ['class' => $item['type'] . ' file_icon']) ?>
|
<?= Html::tag('i', '', ['class' => $item['type'] . ' file_icon']) ?>
|
||||||
<?= Html::a($item['name'], ['vault/download', 'relativePath' => $relativePath], ['class' => 'file_name']) ?>
|
<!-- --><?php //= Html::a($item['name'], ['vault/download', 'relativePath' => $relativePath], ['class' => 'file_name']) ?>
|
||||||
|
<?= Html::beginTag('span', ['class' => 'file_name']) ?>
|
||||||
|
<?= $item['name'] ?>
|
||||||
|
<?= Html::endTag('span') ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="file_info">
|
<td class="file_info">
|
||||||
<?= date('Y-m-d H:i:s', $item['lastModified']) ?>
|
<?= date('Y-m-d H:i:s', $item['lastModified']) ?>
|
||||||
@ -134,7 +137,8 @@ $this->registerCssFile('@web/css/home_style.css');
|
|||||||
'class' => 'btn btn-outline-primary download-btn',
|
'class' => 'btn btn-outline-primary download-btn',
|
||||||
'data-bs-toggle' => 'tooltip',
|
'data-bs-toggle' => 'tooltip',
|
||||||
'data-bs-placement' => 'top',
|
'data-bs-placement' => 'top',
|
||||||
'data-bs-title' => '下载'
|
'data-bs-title' => '下载',
|
||||||
|
'data-filename' => $item['name'],
|
||||||
]) ?>
|
]) ?>
|
||||||
<?= Html::button(Html::tag('i', '', ['class' => 'fa-regular fa-trash-can']), ['value' => $relativePath, 'class' => 'btn btn-outline-danger delete-btn', 'data-bs-toggle' => 'tooltip', 'data-bs-placement' => 'top', 'data-bs-title' => '删除']) ?>
|
<?= Html::button(Html::tag('i', '', ['class' => 'fa-regular fa-trash-can']), ['value' => $relativePath, 'class' => 'btn btn-outline-danger delete-btn', 'data-bs-toggle' => 'tooltip', 'data-bs-placement' => 'top', 'data-bs-title' => '删除']) ?>
|
||||||
</td>
|
</td>
|
||||||
@ -160,5 +164,6 @@ echo Html::endForm();
|
|||||||
|
|
||||||
Modal::end();
|
Modal::end();
|
||||||
$this->registerJsFile('@web/js/vault_script.js', ['depends' => [JqueryAsset::class], 'position' => View::POS_END]);
|
$this->registerJsFile('@web/js/vault_script.js', ['depends' => [JqueryAsset::class], 'position' => View::POS_END]);
|
||||||
|
$this->registerJsFile('@web/js/vault_core.js', ['position' => View::POS_END]);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
121
web/js/vault_core.js
Normal file
121
web/js/vault_core.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
const vaultRawKey = sessionStorage.getItem('vaultRawKey');
|
||||||
|
async function generateEncryptionKeyFromPassword(password) {
|
||||||
|
const passwordBuffer = new TextEncoder().encode(password);
|
||||||
|
const key = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
passwordBuffer,
|
||||||
|
{name: 'PBKDF2'},
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
return await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: new Uint8Array([]),
|
||||||
|
iterations: 100000,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
{name: 'AES-GCM', length: 256},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 加密文件
|
||||||
|
async function encryptFile(file, password) {
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 生成随机 IV
|
||||||
|
const passwordBuffer = new TextEncoder().encode(password);
|
||||||
|
const key = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
passwordBuffer,
|
||||||
|
{name: 'PBKDF2'},
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const derivedKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: new Uint8Array([]),
|
||||||
|
iterations: 100000,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
{name: 'AES-GCM', length: 256},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(event) {
|
||||||
|
const plaintextData = event.target.result;
|
||||||
|
window.crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv }, // 使用随机生成的 IV
|
||||||
|
derivedKey,
|
||||||
|
plaintextData
|
||||||
|
).then(encryptedData => {
|
||||||
|
const encryptedBlob = new Blob([iv, encryptedData], {type: file.type});
|
||||||
|
resolve(encryptedBlob);
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密文件
|
||||||
|
async function decryptFile(encryptedFile, password) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = async function(event) {
|
||||||
|
try {
|
||||||
|
const encryptedData = new Uint8Array(event.target.result);
|
||||||
|
const iv = encryptedData.slice(0, 12); // 从密文中提取 IV
|
||||||
|
const ciphertext = encryptedData.slice(12);
|
||||||
|
const passwordBuffer = new TextEncoder().encode(password);
|
||||||
|
const key = await window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
passwordBuffer,
|
||||||
|
{name: 'PBKDF2'},
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
const derivedKey = await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: new Uint8Array([]),
|
||||||
|
iterations: 100000,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
{name: 'AES-GCM', length: 256},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
const decryptedData = await window.crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv }, // 使用从密文中提取的 IV
|
||||||
|
derivedKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
const decryptedFile = new Blob([decryptedData], { type: encryptedFile.type });
|
||||||
|
resolve(decryptedFile);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileReader.readAsArrayBuffer(encryptedFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function downloadAndDecryptFile(url, password, filename) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const encryptedFile = await response.blob();
|
||||||
|
const decryptedFile = await decryptFile(encryptedFile, password);
|
||||||
|
const blob = new Blob([decryptedFile], {type: decryptedFile.type});
|
||||||
|
const blobURL = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = blobURL;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(blobURL);
|
||||||
|
}
|
61
web/js/vault_gateway_hook.js
Normal file
61
web/js/vault_gateway_hook.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
document.getElementById('gateway-vault-form').addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
sessionStorage.setItem('vaultRawKey', password);
|
||||||
|
this.submit();
|
||||||
|
});
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
if (!(window.crypto && window.crypto.subtle)) {
|
||||||
|
console.log('浏览器不支持 Crypto API');
|
||||||
|
alert('您的浏览器不支持加密功能,故无法使用文件保险箱功能,请使用现代浏览器。');
|
||||||
|
window.location.href = 'index.php?r=site%2Findex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// async function generateEncryptionKeyFromPassword(password) {
|
||||||
|
// const passwordBuffer = new TextEncoder().encode(password);
|
||||||
|
// const key = await window.crypto.subtle.importKey(
|
||||||
|
// 'raw',
|
||||||
|
// passwordBuffer,
|
||||||
|
// {name: 'PBKDF2'},
|
||||||
|
// false,
|
||||||
|
// ['deriveKey']
|
||||||
|
// );
|
||||||
|
// const encryptionKey = await window.crypto.subtle.deriveKey(
|
||||||
|
// {
|
||||||
|
// name: 'PBKDF2',
|
||||||
|
// salt: new Uint8Array([]),
|
||||||
|
// iterations: 100000,
|
||||||
|
// hash: 'SHA-256'
|
||||||
|
// },
|
||||||
|
// key,
|
||||||
|
// {name: 'AES-GCM', length: 256},
|
||||||
|
// false,
|
||||||
|
// ['encrypt', 'decrypt']
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return encryptionKey;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function cryptoKeyToString(cryptoKey) {
|
||||||
|
// return window.crypto.subtle.exportKey('raw', cryptoKey).then(function (keyData) {
|
||||||
|
// return String.fromCharCode.apply(null, new Uint8Array(keyData));
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function stringToCryptoKey(keyString) {
|
||||||
|
// // 将字符串转换为 Uint8Array
|
||||||
|
// var keyData = new Uint8Array(keyString.length);
|
||||||
|
// for (var i = 0; i < keyString.length; ++i) {
|
||||||
|
// keyData[i] = keyString.charCodeAt(i);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 使用 importKey 方法导入 CryptoKey 对象
|
||||||
|
// return window.crypto.subtle.importKey(
|
||||||
|
// 'raw',
|
||||||
|
// keyData,
|
||||||
|
// {name: 'PBKDF2'},
|
||||||
|
// false,
|
||||||
|
// ['deriveKey']
|
||||||
|
// );
|
||||||
|
// }
|
@ -2,8 +2,15 @@ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggl
|
|||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
});
|
});
|
||||||
$(document).on('click', '.download-btn', function () {
|
$(document).on('click', '.download-btn', async function() {
|
||||||
window.location.href = $(this).attr('value');
|
const downloadUrl = $(this).attr('value');
|
||||||
|
const filename = $(this).data('filename');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadAndDecryptFile(downloadUrl, vaultRawKey, filename);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading or decrypting the file:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$(document).on('click', '.delete-btn', function () {
|
$(document).on('click', '.delete-btn', function () {
|
||||||
var relativePath = $(this).attr('value');
|
var relativePath = $(this).attr('value');
|
||||||
@ -16,26 +23,34 @@ $(document).on('click', '.file-upload-btn', function () {
|
|||||||
$('#file-input').on('change', function () {
|
$('#file-input').on('change', function () {
|
||||||
uploadFiles(this.files);
|
uploadFiles(this.files);
|
||||||
});
|
});
|
||||||
$('#folder-input').on('change', function () {
|
|
||||||
uploadFiles(this.files);
|
|
||||||
});
|
|
||||||
$(document).on('click', '.refresh-btn', function () {
|
$(document).on('click', '.refresh-btn', function () {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
$(document).on('click', '.single-download-btn', function () {
|
$(document).on('click', '.single-download-btn', function () {
|
||||||
var relativePath = $('.select-item:checked').first().data('relativePath');
|
var downloadBtn = $('.select-item:checked').closest('tr').find('.download-btn');
|
||||||
window.location.href = 'index.php?r=vault%2Fdownload&relativePath=' + encodeURIComponent(relativePath);
|
if (downloadBtn.length > 0) {
|
||||||
|
downloadBtn.trigger('click');
|
||||||
|
} else {
|
||||||
|
console.error('No file selected for download.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function uploadFiles(files) {
|
async function uploadFiles(files) {
|
||||||
|
// 这里问gpt的,加密方面实在不会
|
||||||
$('#progress-bar').show();
|
$('#progress-bar').show();
|
||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
for (var i = 0; i < files.length; i++) {
|
var encryptionPromises = Array.from(files).map(file => encryptFile(file, vaultRawKey));
|
||||||
formData.append('files[]', files[i]);
|
var encryptedFiles = await Promise.all(encryptionPromises);
|
||||||
}
|
|
||||||
|
encryptedFiles.forEach(function (encryptedFile, index) {
|
||||||
|
formData.append('files[]', new File([encryptedFile], files[index].name, {type: files[index].type}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加其他数据到 FormData 中
|
||||||
formData.append('targetDir', $('#target-dir').val());
|
formData.append('targetDir', $('#target-dir').val());
|
||||||
formData.append('_csrf', $('meta[name="csrf-token"]').attr('content'));
|
formData.append('_csrf', $('meta[name="csrf-token"]').attr('content'));
|
||||||
|
|
||||||
|
// 创建 XMLHttpRequest 对象并发送 FormData
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.upload.onprogress = function (event) {
|
xhr.upload.onprogress = function (event) {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
|
Loading…
Reference in New Issue
Block a user