完成sftp储存策略

This commit is contained in:
Wisp X 2022-03-05 12:10:10 +08:00
parent 2a7dd080ef
commit 68a1a8664a
8 changed files with 176 additions and 12 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace App\Enums\Strategy;
final class SftpOption
{
/** @var string 访问url */
const Url = 'url';
/** @var string 根目录 */
const Root = 'root';
/** @var string 主机地址 */
const Host = 'host';
/** @var string 连接名称 */
const Username = 'username';
/** @var string 密码 */
const Password = 'password';
/** @var string 私钥 */
const PrivateKey = 'private_key';
/** @var string 密钥口令 */
const Passphrase = 'passphrase';
/** @var string 连接端口 */
const Port = 'port';
/** @var string 使用代理 */
const UseAgent = 'use_agent';
}

View File

@ -82,6 +82,16 @@ class StrategyRequest extends FormRequest
'configs.secret_key' => 'required',
'configs.bucket' => 'required',
],
StrategyKey::Sftp => [
'configs.root' => 'required',
'configs.host' => 'required',
'configs.port' => 'integer',
'configs.username' => 'required',
'configs.password' => 'required_if:configs.private_key,null',
'configs.private_key' => 'required_if:configs.password,null',
'configs.passphrase' => '',
'configs.use_agent' => 'boolean',
],
});
}
@ -110,6 +120,16 @@ class StrategyRequest extends FormRequest
'configs.secret_key' => 'SecretKey',
'configs.bucket' => 'Bucket',
],
StrategyKey::Sftp => [
'configs.root' => '储存路径',
'configs.host' => '主机地址',
'configs.port' => '连接端口',
'configs.username' => '用户名',
'configs.password' => '密码',
'configs.private_key' => '密钥',
'configs.passphrase' => '密钥口令',
'configs.use_agent' => '使用代理',
],
});
}
}

View File

@ -10,6 +10,7 @@ use App\Enums\ImagePermission;
use App\Enums\Scan\AliyunOption;
use App\Enums\Strategy\CosOption;
use App\Enums\Strategy\KodoOption;
use App\Enums\Strategy\SftpOption;
use App\Enums\StrategyKey;
use App\Enums\UserConfigKey;
use App\Enums\UserStatus;
@ -28,6 +29,7 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image as InterventionImage;
use Intervention\Image\Imagick\Font;
@ -36,6 +38,9 @@ use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;
use League\Flysystem\Local\LocalFilesystemAdapter;
use League\Flysystem\PhpseclibV2\SftpAdapter;
use League\Flysystem\PhpseclibV2\SftpConnectionProvider;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use Overtrue\Flysystem\Cos\CosAdapter;
use Overtrue\Flysystem\Qiniu\QiniuAdapter;
@ -151,7 +156,8 @@ class ImageService
try {
$filesystem->writeStream($pathname, $handle);
} catch (FilesystemException $e) {
throw new UploadException('图片上传失败');
Log::error('保存图片时出现异常', ['message' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
throw new UploadException(config('app.debug', false) ? $e->getMessage() : '图片上传失败');
}
@fclose($handle);
} else {
@ -211,6 +217,27 @@ class ImageService
bucket: $configs->get(KodoOption::Bucket),
domain: $configs->get(KodoOption::Url),
),
StrategyKey::Sftp => new SftpAdapter(new SftpConnectionProvider(
host: $configs->get(SftpOption::Host),
username: $configs->get(SftpOption::Username),
password: $configs->get(SftpOption::Password),
privateKey: $configs->get(SftpOption::PrivateKey),
passphrase: (string)$configs->get(SftpOption::Passphrase),
port: $configs->get(SftpOption::Port),
useAgent: (bool)$configs->get(SftpOption::UseAgent)
),
root: $configs->get(SftpOption::Root),
visibilityConverter: PortableVisibilityConverter::fromArray([
'file' => [
'public' => 0640,
'private' => 0604,
],
'dir' => [
'public' => 0740,
'private' => 7604,
],
])
)
};
}

View File

@ -40,26 +40,20 @@
<form action="{{ route('admin.settings.save') }}">
<div class="relative p-4 rounded-md bg-white mb-8 space-y-4">
<x-fieldset title="是否启用注册" faq="启用或关闭系统注册功能">
<input type="hidden" name="is_enable_registration" value="0">
<x-switch name="is_enable_registration" value="1" :checked="(bool) $configs['is_enable_registration']" />
</x-fieldset>
<x-fieldset title="是否启用画廊" faq="启用或关闭画廊功能,画廊只有已登录的用户可见,画廊中的图片均为所有用户公开的图片。">
<input type="hidden" name="is_enable_gallery" value="0">
<x-switch name="is_enable_gallery" value="1" :checked="(bool) $configs['is_enable_gallery']" />
</x-fieldset>
<x-fieldset title="是否启用接口" faq="启用或关闭接口功能,关闭后将无法通过接口上传图片、管理图片等操作。">
<input type="hidden" name="is_enable_api" value="0">
<x-switch name="is_enable_api" value="1" :checked="(bool) $configs['is_enable_api']" />
</x-fieldset>
<x-fieldset title="是否允许游客上传" faq="启用或关闭游客上传功能,游客上传受「系统默认组」控制。">
<input type="hidden" name="is_allow_guest_upload" value="0">
<x-switch name="is_allow_guest_upload" value="1" :checked="(bool) $configs['is_allow_guest_upload']" />
</x-fieldset>
<x-fieldset title="账号验证" faq="是否强制用户验证邮箱,开启后用户必须经过验证邮箱后才能上传图片,请确保邮件配置正常。">
<input type="hidden" name="is_user_need_verify" value="0">
<x-switch name="is_user_need_verify" value="1" :checked="(bool) $configs['is_user_need_verify']" />
</x-fieldset>
<div class="text-right">
<x-button type="submit">保存更改</x-button>
</div>

View File

@ -100,6 +100,47 @@
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket" />
</div>
</div>
<div class="col-span-6 mb-4 hidden" data-driver="{{ \App\Enums\StrategyKey::Sftp }}">
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div>
<div class="col-span-6">
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="/" />
</div>
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
<x-input type="text" name="configs[host]" id="configs[host]" placeholder="请输入主机地址例如127.0.0.1" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label>
<x-input type="number" name="configs[port]" id="configs[port]" placeholder="请输入连接端口" value="22" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label>
<x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>密码</label>
<x-input type="password" name="configs[password]" id="configs[password]" placeholder="如果使用私钥连接,可为空" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[private_key]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>私钥</label>
<x-textarea name="configs[private_key]" id="configs[private_key]" placeholder="输入私钥文本内容,如果使用密码连接,可为空"></x-textarea>
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[passphrase]" class="block text-sm font-medium text-gray-700">私钥口令</label>
<x-input type="password" name="configs[passphrase]" id="configs[passphrase]" placeholder="如果未设置私钥或私钥未设置口令,可为空" />
</div>
<div class="col-span-6">
<label for="configs[use_agent]" class="block text-sm font-medium mb-2 text-gray-700">是否使用代理</label>
<x-switch id="configs[use_agent]" name="configs[use_agent]" value="1"></x-switch>
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<x-button type="button" class="bg-gray-500" onclick="history.go(-1)">取消</x-button>

View File

@ -37,6 +37,7 @@
<option value="{{ $key }}" {{ $key === $strategy->key ? 'selected' : '' }}>{{ $driver }}</option>
@endforeach
</x-select>
<input type="hidden" name="key" value="{{ $strategy->key }}">
<small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 为了确保已存在于该储存上的图片能够在平台正常预览,已创建的策略无法继续更改储存方式。</small>
</div>
@ -108,6 +109,49 @@
</div>
</div>
@endif
@if($strategy->key === \App\Enums\StrategyKey::Sftp)
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Sftp }}">
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" />
</div>
<div class="col-span-6">
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="{{ $strategy->configs['root'] }}" />
</div>
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
<x-input type="text" name="configs[host]" id="configs[host]" placeholder="请输入主机地址例如127.0.0.1" value="{{ $strategy->configs['host'] }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label>
<x-input type="number" name="configs[port]" id="configs[port]" placeholder="请输入连接端口" value="{{ $strategy->configs['port'] }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label>
<x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs['username'] }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>密码</label>
<x-input type="password" name="configs[password]" id="configs[password]" placeholder="如果使用私钥连接,可为空" value="{{ $strategy->configs['password'] }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[private_key]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>私钥</label>
<x-textarea name="configs[private_key]" id="configs[private_key]" placeholder="输入私钥文本内容,如果使用密码连接,可为空">{{ $strategy->configs['private_key'] }}</x-textarea>
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[passphrase]" class="block text-sm font-medium text-gray-700">私钥口令</label>
<x-input type="password" name="configs[passphrase]" id="configs[passphrase]" placeholder="如果未设置私钥或私钥未设置口令,可为空" value="{{ $strategy->configs['passphrase'] }}" />
</div>
<div class="col-span-6">
<label for="configs[use_agent]" class="block text-sm font-medium mb-2 text-gray-700">是否使用代理</label>
<x-switch id="configs[use_agent]" name="configs[use_agent]" value="1" :checked="(bool)$strategy->configs['use_agent']"></x-switch>
</div>
</div>
@endif
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<x-button type="button" class="bg-gray-500" onclick="history.go(-1)">取消</x-button>

View File

@ -49,7 +49,7 @@
</div>
<div class="relative flex w-full">
<div class="w-10 h-10 bg-gray-200 rounded-lg cursor-pointer overflow-hidden">
<img class="w-full h-full" src="__src__">
<img class="w-full h-full object-cover" src="__src__">
</div>
<div class="flex justify-end flex-col ml-2 w-[80%] opacity-70">
<p class="text-sm truncate">__name__</p>

View File

@ -48,12 +48,17 @@
</body>
<script>
// 开关组件默认值
$('.switch input[type=checkbox]').click(function () {
if (this.checked) {
$(this).closest('.switch').find('input[type=hidden]').remove();
let setSwitch = function (e) {
if (e.checked) {
$(e).closest('.switch').find('input[type=hidden]').remove();
} else {
$(this).before('<input type="hidden" name="'+this.name+'" value="0" />');
$(e).before('<input type="hidden" name="'+e.name+'" value="0" />');
}
}
$('.switch input[type=checkbox]').each(function () {
setSwitch(this);
}).click(function () {
setSwitch(this);
});
</script>
@stack('scripts')