Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: complete folder upload #1015

Merged
merged 2 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/layout/AppUploadAndPrintBtn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ export default class AppUploadAndPrintBtn extends Vue {
fileChanged (e: Event) {
const target = e.target as HTMLInputElement

if (target?.files?.length === 1) {
this.$emit('upload', target.files[0])
if (target) {
if (target.files?.length === 1) {
this.$emit('upload', target.files[0])
}

target.value = ''
}
}
Expand Down
14 changes: 11 additions & 3 deletions src/components/widgets/filesystem/FileSystem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import FileSystemUploadDialog from './FileSystemUploadDialog.vue'
import FilePreviewDialog from './FilePreviewDialog.vue'
import Axios from 'axios'
import { AppTableHeader } from '@/types'
import { FileWithPath, getFilesFromDataTransfer } from '@/util/file-system-entry'

/**
* Represents the filesystem, bound to moonrakers supplied roots.
Expand Down Expand Up @@ -821,7 +822,7 @@ export default class FileSystem extends Mixins(StateMixin, FilesMixin, ServicesM
}
}

async handleUpload (files: FileList | File[], print: boolean) {
async handleUpload (files: FileList | File[] | FileWithPath[], print: boolean) {
this.$store.dispatch('wait/addWait', this.$waits.onFileSystem)
this.uploadFiles(files, this.visiblePath, this.currentRoot, print)
this.$store.dispatch('wait/removeWait', this.$waits.onFileSystem)
Expand Down Expand Up @@ -912,8 +913,15 @@ export default class FileSystem extends Mixins(StateMixin, FilesMixin, ServicesM

async handleDropFile (e: DragEvent) {
this.dragState.overlay = false
if (e && e.dataTransfer && e.dataTransfer.files.length && !this.rootProperties.readonly) {
this.handleUpload(e.dataTransfer.files, false)

if (!e.dataTransfer || this.rootProperties.readonly) {
return
}

const files = await getFilesFromDataTransfer(e.dataTransfer)

if (files) {
this.handleUpload(files, false)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<v-icon x-large>
$fileUpload
</v-icon>
<span v-html="$t('app.file_system.overlay.label')" />
<span v-html="$t('app.file_system.overlay.drag_files_folders')" />
</v-col>
</v-row>
</v-container>
Expand Down
34 changes: 25 additions & 9 deletions src/components/widgets/filesystem/FileSystemMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,22 @@
>
$fileUpload
</v-icon>
{{ $t('app.general.btn.upload') }}
{{ $t('app.general.btn.upload_files') }}
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="!readonly"
:disabled="disabled"
@click="emulateClick(false, true)"
>
<v-list-item-title>
<v-icon
small
left
>
$folderUpload
</v-icon>
{{ $t('app.general.btn.upload_folder') }}
</v-list-item-title>
</v-list-item>
<v-list-item
Expand Down Expand Up @@ -101,6 +116,7 @@
</template>

<script lang="ts">
import { getFilesWithPathFromHTMLInputElement } from '@/util/file-system-entry'
import { Component, Vue, Prop, Ref } from 'vue-property-decorator'

@Component({})
Expand Down Expand Up @@ -129,23 +145,23 @@ export default class FileSystemMenu extends Vue {
return this.$store.getters['files/getRootProperties'](this.root).canCreateDirectory
}

emulateClick (startPrint: boolean) {
emulateClick (startPrint: boolean, folder = false) {
this.andPrint = startPrint
this.uploadFile.multiple = !startPrint // Can't start print with multiple files
this.uploadFile.webkitdirectory = folder
this.uploadFile.click()
}

fileChanged (e: Event) {
async fileChanged (e: Event) {
const target = e.target as HTMLInputElement
const files = target.files
const fileList = []

if (target && files && files.length > 0) {
for (let i = 0; i < files.length; i++) {
fileList.push(files[i])
if (target) {
const files = await getFilesWithPathFromHTMLInputElement(target)

if (files) {
this.$emit('upload', files, this.andPrint)
}

this.$emit('upload', fileList, this.andPrint)
target.value = ''
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ import {
mdiArchive,
mdiArchivePlus,
mdiTrayFull,
mdiTrayPlus
mdiTrayPlus,
mdiFolderArrowUp
} from '@mdi/js'

/**
Expand Down Expand Up @@ -253,6 +254,7 @@ export const Icons = Object.freeze({
alertCircle: mdiAlertCircle,
folderAdd: mdiFolderPlus,
folderUp: mdiFolderUpload,
folderUpload: mdiFolderArrowUp,
folder: mdiFolder,
fileUpload: mdiUpload,
fileAdd: mdiFilePlus,
Expand Down
4 changes: 3 additions & 1 deletion src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ app:
not_found: No files found
processing: Processing
overlay:
label: <strong>Drag</strong> a file here
drag_files_folders: <strong>Drag</strong> files and folders here
title:
add_dir: Add Directory
add_file: Add File
Expand Down Expand Up @@ -196,6 +196,8 @@ app:
socket_reconnect: Re-Connect
socket_refresh: Force refresh
upload: Upload
upload_files: Upload Files
upload_folder: Upload Folder
upload_print: Upload & Print
view: View
reset_stats: Reset Stats
Expand Down
34 changes: 27 additions & 7 deletions src/mixins/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Vue from 'vue'
import { Component } from 'vue-property-decorator'
import Axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'
import { httpClientActions } from '@/api/httpClientActions'
import { FileWithPath } from '@/util/file-system-entry'

@Component
export default class FilesMixin extends Vue {
Expand Down Expand Up @@ -208,11 +209,12 @@ export default class FilesMixin extends Vue {
async uploadFile (file: File, path: string, root: string, andPrint: boolean, options?: AxiosRequestConfig) {
const formData = new FormData()
// let filename = file.name.replace(' ', '_')
let filepath = `${path}/${file.name}`
let filepath = `${path}${file.name}`
filepath = (filepath.startsWith('/'))
? filepath
: '/' + filepath
formData.append('file', file, filepath)
formData.append('file', file, file.name)
formData.append('path', path)
formData.append('root', root)
if (andPrint) {
formData.append('print', 'true')
Expand Down Expand Up @@ -266,17 +268,33 @@ export default class FilesMixin extends Vue {
})
}

getFullPathAndFile (rootPath: string, file: File | FileWithPath): [string, File] {
if ('path' in file) {
return [
`${rootPath}/${file.path}`,
file.file
]
} else {
return [
rootPath,
file
]
}
}

// Upload some files.
async uploadFiles (files: FileList | File[], path: string, root: string, andPrint: boolean) {
async uploadFiles (files: FileList | File[] | FileWithPath[], path: string, root: string, andPrint: boolean) {
// For each file, adds the associated state.
for (const file of files) {
let filepath = `${path}/${file.name}`
const [fullPath, fileObject] = this.getFullPathAndFile(path, file)

let filepath = `${fullPath}${fileObject.name}`
filepath = (filepath.startsWith('/'))
? filepath
: '/' + filepath
this.$store.dispatch('files/updateFileUpload', {
filepath,
size: file.size,
size: fileObject.size,
loaded: 0,
percent: 0,
speed: 0,
Expand All @@ -290,7 +308,9 @@ export default class FilesMixin extends Vue {
// processing of each file.
if (files.length > 1) andPrint = false
for (const file of files) {
let filepath = `${path}/${file.name}`
const [fullPath, fileObject] = this.getFullPathAndFile(path, file)

let filepath = `${fullPath}${fileObject.name}`
filepath = (filepath.startsWith('/'))
? filepath
: '/' + filepath
Expand All @@ -299,7 +319,7 @@ export default class FilesMixin extends Vue {
if (fileState && !fileState?.cancelled) {
try {
this.cancelTokenSource = Axios.CancelToken.source()
await this.uploadFile(file, path, root, andPrint, {
await this.uploadFile(fileObject, fullPath, root, andPrint, {
cancelToken: this.cancelTokenSource.token
})
} catch (e) {
Expand Down
96 changes: 96 additions & 0 deletions src/util/file-system-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import consola from 'consola'

type EntryWithPath = {
entry: FileSystemEntry,
path: string
}
export type FileWithPath = {
file: File,
path: string
}

const isFile = (item: FileSystemEntry): item is FileSystemFileEntry => item.isFile

const isDirectory = (item: FileSystemEntry): item is FileSystemDirectoryEntry => item.isDirectory

const getFileAsync = async (fileEntry: FileSystemFileEntry) => {
try {
return new Promise<File>((resolve, reject) => fileEntry.file(resolve, reject))
} catch (e) {
consola.error('[FileSystemFileEntry] file', e)
}
}

const readEntriesAsync = async (directoryReader: FileSystemDirectoryReader) => {
try {
return new Promise<FileSystemEntry[]>((resolve, reject) => directoryReader.readEntries(resolve, reject))
} catch (e) {
consola.error('[FileSystemDirectoryReader] readEntries', e)
}
}

export const getFilesFromDataTransfer = async (dataTransfer: DataTransfer) => {
if (dataTransfer.items.length) {
const entries = [...dataTransfer.items]
.map(x => x.webkitGetAsEntry())
.filter((x): x is FileSystemEntry => !!x)

return await getFilesFromFileSystemEntries(entries)
} else if (dataTransfer.files.length) {
return convertFilesToFilesWithPath(dataTransfer.files)
}
}

export const getFilesWithPathFromHTMLInputElement = async (input: HTMLInputElement) => {
if (input.webkitEntries.length) {
return await getFilesFromFileSystemEntries(input.webkitEntries)
} else if (input.files?.length) {
return convertFilesToFilesWithPath(input.files)
}
}

export const convertFilesToFilesWithPath = (files: File[] | FileList) => {
return [...files]
.map(file => ({
file,
path: file.webkitRelativePath.slice(0, -file.name.length)
} as FileWithPath))
}

export const getFilesFromFileSystemEntries = async (entries: readonly FileSystemEntry[]) => {
const files: FileWithPath[] = []
const items = entries
.map(entry => ({
entry,
path: ''
} as EntryWithPath))

let item = items.pop()
while (item) {
if (isFile(item.entry)) {
const file = await getFileAsync(item.entry)

if (file) {
files.push({
file,
path: item.path
})
}
} else if (isDirectory(item.entry)) {
const subEntries = await readEntriesAsync(item.entry.createReader())

if (subEntries) {
for (const entry of subEntries) {
items.push({
entry,
path: item.path + item.entry.name + '/'
})
}
}
}

item = items.pop()
}

return files
}