mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: 增加创建文件夹功能
This commit is contained in:
parent
e0757f4d65
commit
f48ae22a15
@ -34,3 +34,17 @@ func (b *BaseApi) GetFileTree(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
helper.SuccessWithData(c, tree)
|
helper.SuccessWithData(c, tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CreateFile(c *gin.Context) {
|
||||||
|
var req dto.FileCreate
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := fileService.Create(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "github.com/1Panel-dev/1Panel/utils/files"
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/utils/files"
|
||||||
|
)
|
||||||
|
|
||||||
type FileOption struct {
|
type FileOption struct {
|
||||||
files.FileOption
|
files.FileOption
|
||||||
@ -16,3 +18,10 @@ type FileTree struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Children []FileTree `json:"children"`
|
Children []FileTree `json:"children"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileCreate struct {
|
||||||
|
Path string
|
||||||
|
Content string
|
||||||
|
IsDir bool
|
||||||
|
Mode int64
|
||||||
|
}
|
||||||
|
@ -6,19 +6,12 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/app/dto"
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/utils/files"
|
"github.com/1Panel-dev/1Panel/utils/files"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileService struct {
|
type FileService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type IFileService interface {
|
|
||||||
GetFileList(op dto.FileOption) (dto.FileInfo, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFileService() IFileService {
|
|
||||||
return FileService{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FileService) GetFileList(op dto.FileOption) (dto.FileInfo, error) {
|
func (f FileService) GetFileList(op dto.FileOption) (dto.FileInfo, error) {
|
||||||
var fileInfo dto.FileInfo
|
var fileInfo dto.FileInfo
|
||||||
info, err := files.NewFileInfo(op.FileOption)
|
info, err := files.NewFileInfo(op.FileOption)
|
||||||
@ -52,6 +45,16 @@ func (f FileService) GetFileTree(op dto.FileOption) ([]dto.FileTree, error) {
|
|||||||
return append(treeArray, node), nil
|
return append(treeArray, node), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f FileService) Create(op dto.FileCreate) error {
|
||||||
|
|
||||||
|
fo := files.NewFileOp()
|
||||||
|
if op.IsDir {
|
||||||
|
return fo.CreateDir(op.Path, fs.FileMode(op.Mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getUuid() string {
|
func getUuid() string {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
io.ReadFull(rand.Reader, b)
|
io.ReadFull(rand.Reader, b)
|
||||||
|
59
backend/init/cache/cache.go
vendored
59
backend/init/cache/cache.go
vendored
@ -1,18 +1,75 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
"github.com/1Panel-dev/1Panel/init/cache/badger_db"
|
"github.com/1Panel-dev/1Panel/init/cache/badger_db"
|
||||||
"github.com/dgraph-io/badger/v3"
|
"github.com/dgraph-io/badger/v3"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
c := global.CONF.Cache
|
c := global.CONF.Cache
|
||||||
|
|
||||||
cache, err := badger.Open(badger.DefaultOptions(c.Path))
|
options := badger.Options{
|
||||||
|
Dir: c.Path,
|
||||||
|
ValueDir: c.Path,
|
||||||
|
ValueLogFileSize: 102400000,
|
||||||
|
ValueLogMaxEntries: 100000,
|
||||||
|
VLogPercentile: 0.1,
|
||||||
|
|
||||||
|
MemTableSize: 64 << 20,
|
||||||
|
BaseTableSize: 2 << 20,
|
||||||
|
BaseLevelSize: 10 << 20,
|
||||||
|
TableSizeMultiplier: 2,
|
||||||
|
LevelSizeMultiplier: 10,
|
||||||
|
MaxLevels: 7,
|
||||||
|
NumGoroutines: 8,
|
||||||
|
MetricsEnabled: true,
|
||||||
|
NumCompactors: 4,
|
||||||
|
NumLevelZeroTables: 5,
|
||||||
|
NumLevelZeroTablesStall: 15,
|
||||||
|
NumMemtables: 5,
|
||||||
|
BloomFalsePositive: 0.01,
|
||||||
|
BlockSize: 4 * 1024,
|
||||||
|
SyncWrites: false,
|
||||||
|
NumVersionsToKeep: 1,
|
||||||
|
CompactL0OnClose: false,
|
||||||
|
VerifyValueChecksum: false,
|
||||||
|
BlockCacheSize: 256 << 20,
|
||||||
|
IndexCacheSize: 0,
|
||||||
|
ZSTDCompressionLevel: 1,
|
||||||
|
EncryptionKey: []byte{},
|
||||||
|
EncryptionKeyRotationDuration: 10 * 24 * time.Hour, // Default 10 days.
|
||||||
|
DetectConflicts: true,
|
||||||
|
NamespaceOffset: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
cache, err := badger.Open(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
global.CACHE = badger_db.NewCacheDB(cache)
|
global.CACHE = badger_db.NewCacheDB(cache)
|
||||||
|
|
||||||
|
err = cache.View(func(txn *badger.Txn) error {
|
||||||
|
opts := badger.DefaultIteratorOptions
|
||||||
|
opts.PrefetchValues = false
|
||||||
|
it := txn.NewIterator(opts)
|
||||||
|
defer it.Close()
|
||||||
|
for it.Rewind(); it.Valid(); it.Next() {
|
||||||
|
item := it.Item()
|
||||||
|
k := item.Key()
|
||||||
|
fmt.Printf("key=%s\n", k)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("run gc")
|
||||||
|
err = cache.RunValueLogGC(0.01)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
|
|||||||
{
|
{
|
||||||
fileRouter.POST("/search", baseApi.ListFiles)
|
fileRouter.POST("/search", baseApi.ListFiles)
|
||||||
fileRouter.POST("/tree", baseApi.GetFileTree)
|
fileRouter.POST("/tree", baseApi.GetFileTree)
|
||||||
|
fileRouter.POST("", baseApi.CreateFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
44
backend/utils/files/file_op.go
Normal file
44
backend/utils/files/file_op.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileOp struct {
|
||||||
|
Fs afero.Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileOp() FileOp {
|
||||||
|
return FileOp{
|
||||||
|
Fs: afero.NewOsFs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileOp) CreateDir(dst string, mode fs.FileMode) error {
|
||||||
|
return f.Fs.MkdirAll(dst, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error {
|
||||||
|
dir, _ := path.Split(dst)
|
||||||
|
if err := f.Fs.MkdirAll(dir, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(file, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = file.Stat(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,6 +1,4 @@
|
|||||||
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
// import { showFullScreenLoading, tryHideFullScreenLoading } from '@/config/service-loading';
|
|
||||||
import { AxiosCanceler } from './helper/axios-cancel';
|
|
||||||
import { ResultData } from '@/api/interface';
|
import { ResultData } from '@/api/interface';
|
||||||
import { ResultEnum } from '@/enums/http-enum';
|
import { ResultEnum } from '@/enums/http-enum';
|
||||||
import { checkStatus } from './helper/check-status';
|
import { checkStatus } from './helper/check-status';
|
||||||
@ -9,7 +7,6 @@ import router from '@/routers';
|
|||||||
import { GlobalStore } from '@/store';
|
import { GlobalStore } from '@/store';
|
||||||
|
|
||||||
const globalStore = GlobalStore();
|
const globalStore = GlobalStore();
|
||||||
const axiosCanceler = new AxiosCanceler();
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
baseURL: import.meta.env.VITE_API_URL as string,
|
baseURL: import.meta.env.VITE_API_URL as string,
|
||||||
@ -30,8 +27,7 @@ class RequestHttp {
|
|||||||
...config.headers,
|
...config.headers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
axiosCanceler.addPending(config);
|
|
||||||
// config.headers!.noLoading || showFullScreenLoading();
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
@ -43,12 +39,10 @@ class RequestHttp {
|
|||||||
|
|
||||||
this.service.interceptors.response.use(
|
this.service.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(response: AxiosResponse) => {
|
||||||
const { data, config } = response;
|
const { data } = response;
|
||||||
if (response.headers['x-csrf-token']) {
|
if (response.headers['x-csrf-token']) {
|
||||||
globalStore.setCsrfToken(response.headers['x-csrf-token']);
|
globalStore.setCsrfToken(response.headers['x-csrf-token']);
|
||||||
}
|
}
|
||||||
axiosCanceler.removePending(config);
|
|
||||||
// tryHideFullScreenLoading();
|
|
||||||
if (data.code == ResultEnum.OVERDUE || data.code == ResultEnum.FORBIDDEN) {
|
if (data.code == ResultEnum.OVERDUE || data.code == ResultEnum.FORBIDDEN) {
|
||||||
ElMessage.error(data.msg);
|
ElMessage.error(data.msg);
|
||||||
router.replace({
|
router.replace({
|
||||||
@ -64,7 +58,6 @@ class RequestHttp {
|
|||||||
},
|
},
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
// tryHideFullScreenLoading();
|
|
||||||
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试');
|
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试');
|
||||||
if (response) checkStatus(response.status);
|
if (response) checkStatus(response.status);
|
||||||
if (!window.navigator.onLine) router.replace({ path: '/500' });
|
if (!window.navigator.onLine) router.replace({ path: '/500' });
|
||||||
|
@ -29,4 +29,10 @@ export namespace File {
|
|||||||
path: string;
|
path: string;
|
||||||
children?: FileTree[];
|
children?: FileTree[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileCreate {
|
||||||
|
path: string;
|
||||||
|
isDir: Boolean;
|
||||||
|
mode: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,3 +8,7 @@ export const GetFilesList = (params: File.ReqFile) => {
|
|||||||
export const GetFilesTree = (params: File.ReqFile) => {
|
export const GetFilesTree = (params: File.ReqFile) => {
|
||||||
return http.post<File.FileTree[]>('files/tree', params);
|
return http.post<File.FileTree[]>('files/tree', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CreateFile = (form: File.FileCreate) => {
|
||||||
|
return http.post<File.File>('files', form);
|
||||||
|
};
|
||||||
|
@ -176,5 +176,6 @@ export default {
|
|||||||
zip: '压缩',
|
zip: '压缩',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
group: '组',
|
group: '组',
|
||||||
|
path: '路径',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
70
frontend/src/views/file-management/create.vue
Normal file
70
frontend/src/views/file-management/create.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="open"
|
||||||
|
:before-close="handleClose"
|
||||||
|
:title="$t('commons.button.create')"
|
||||||
|
width="30%"
|
||||||
|
@open="onOpen"
|
||||||
|
v-loading="loading"
|
||||||
|
>
|
||||||
|
<el-form ref="fileForm" label-position="left" :model="form">
|
||||||
|
<el-form-item :label="$t('file.path')"> <el-input v-model="form.path" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="submit(fileForm)">{{ $t('commons.button.confirm') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits, toRefs, ref } from 'vue';
|
||||||
|
import { File } from '@/api/interface/file';
|
||||||
|
import { ElMessage, FormInstance } from 'element-plus';
|
||||||
|
import { CreateFile } from '@/api/modules/files';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
|
||||||
|
const fileForm = ref<FormInstance>();
|
||||||
|
let loading = ref<Boolean>(false);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: Boolean,
|
||||||
|
file: Object,
|
||||||
|
});
|
||||||
|
const { open, file } = toRefs(props);
|
||||||
|
let form = ref<File.FileCreate>({ path: '', isDir: false, mode: 0o755 });
|
||||||
|
const em = defineEmits(['close']);
|
||||||
|
const handleClose = () => {
|
||||||
|
em('close', open);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
await formEl.validate((valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
CreateFile(form.value)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.createSuccess'));
|
||||||
|
handleClose();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpen = () => {
|
||||||
|
const f = file?.value as File.FileCreate;
|
||||||
|
form.value.isDir = f.isDir;
|
||||||
|
form.value.path = f.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
// function PrefixInteger(num: number, length: number) {
|
||||||
|
// return (Array(length).join('0') + num).slice(-length);
|
||||||
|
// }
|
||||||
|
</script>
|
@ -43,17 +43,17 @@
|
|||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
>
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<el-dropdown split-button type="primary">
|
<el-dropdown split-button type="primary" @command="handleCreate">
|
||||||
{{ $t('commons.button.create') }}
|
{{ $t('commons.button.create') }}
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item>
|
<el-dropdown-item command="dir">
|
||||||
<svg-icon iconName="p-file-folder"></svg-icon>{{ $t('file.dir') }}
|
<svg-icon iconName="p-file-folder"></svg-icon>{{ $t('file.dir') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item>
|
<el-dropdown-item command="file">
|
||||||
<svg-icon iconName="p-file-normal"></svg-icon>{{ $t('file.file') }}
|
<svg-icon iconName="p-file-normal"></svg-icon>{{ $t('file.file') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item>
|
<el-dropdown-item command="link">
|
||||||
<svg-icon iconName="p-file-normal"></svg-icon>{{ $t('file.linkFile') }}
|
<svg-icon iconName="p-file-normal"></svg-icon>{{ $t('file.linkFile') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
@ -94,6 +94,7 @@
|
|||||||
/>
|
/>
|
||||||
</ComplexTable>
|
</ComplexTable>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<CreateFile :open="openCreate" :file="fileCreate" @close="close"></CreateFile>
|
||||||
</el-row>
|
</el-row>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</template>
|
</template>
|
||||||
@ -108,6 +109,7 @@ import { dateFromat } from '@/utils/util';
|
|||||||
import { File } from '@/api/interface/file';
|
import { File } from '@/api/interface/file';
|
||||||
import BreadCrumbs from '@/components/bread-crumbs/index.vue';
|
import BreadCrumbs from '@/components/bread-crumbs/index.vue';
|
||||||
import BreadCrumbItem from '@/components/bread-crumbs/bread-crumbs-item.vue';
|
import BreadCrumbItem from '@/components/bread-crumbs/bread-crumbs-item.vue';
|
||||||
|
import CreateFile from './create.vue';
|
||||||
|
|
||||||
let data = ref();
|
let data = ref();
|
||||||
let selects = ref<any>([]);
|
let selects = ref<any>([]);
|
||||||
@ -117,6 +119,8 @@ let treeLoading = ref<boolean>(false);
|
|||||||
let paths = ref<string[]>([]);
|
let paths = ref<string[]>([]);
|
||||||
let fileTree = ref<File.FileTree[]>([]);
|
let fileTree = ref<File.FileTree[]>([]);
|
||||||
let expandKeys = ref<string[]>([]);
|
let expandKeys = ref<string[]>([]);
|
||||||
|
let openCreate = ref<boolean>(false);
|
||||||
|
let fileCreate = ref<File.FileCreate>({ path: '/', isDir: false, mode: 0o755 });
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
children: 'children',
|
children: 'children',
|
||||||
@ -205,6 +209,23 @@ const loadNode = (node: any, resolve: (data: File.FileTree[]) => void) => {
|
|||||||
}
|
}
|
||||||
resolve([]);
|
resolve([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreate = (commnad: string) => {
|
||||||
|
fileCreate.value.path = req.path;
|
||||||
|
fileCreate.value.isDir = false;
|
||||||
|
if (commnad === 'dir') {
|
||||||
|
fileCreate.value.isDir = true;
|
||||||
|
console.log(fileCreate.value);
|
||||||
|
}
|
||||||
|
console.log(commnad);
|
||||||
|
openCreate.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
openCreate.value = false;
|
||||||
|
search(req);
|
||||||
|
};
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
label: i18n.global.t('file.open'),
|
label: i18n.global.t('file.open'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user