Skip to content

Commit

Permalink
feat: complete folder upload
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
  • Loading branch information
pedrolamas committed Jan 22, 2023
1 parent 85c214e commit 58a113d
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 22 deletions.
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
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
2 changes: 2 additions & 0 deletions src/locales/en.yaml
Original file line number Diff line number Diff line change
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
}

0 comments on commit 58a113d

Please sign in to comment.