diff --git a/packages/dashboard-v2/src/appUtils.js b/packages/dashboard-v2/src/appUtils.js index 9b1fbfa..1dd29ec 100644 --- a/packages/dashboard-v2/src/appUtils.js +++ b/packages/dashboard-v2/src/appUtils.js @@ -176,6 +176,15 @@ export const apiHandler = { onUploadProgress: callback }) }, + listObjects: async (bucket, prefix, delimiter = '/', cursor = null) => { + return await api.get(`/buckets/${bucket}?include=customMetadata&include=httpMetadata`, { + params: { + delimiter: delimiter, + prefix: prefix && prefix !== '/' ? encode(prefix) : '', + cursor: cursor + } + }) + }, fetchFile: async (bucket, prefix, delimiter = '/') => { const mainStore = useMainStore(); let truncated = true @@ -184,13 +193,7 @@ export const apiHandler = { let contentFolders = [] while (truncated) { - const response = await api.get(`/buckets/${bucket}?include=customMetadata&include=httpMetadata`, { - params: { - delimiter: delimiter, - prefix: prefix && prefix !== '/' ? encode(prefix) : '', - cursor: cursor - } - }) + const response = await this.listObjects(bucket, prefix, delimiter, cursor) truncated = response.data.truncated cursor = response.data.cursor diff --git a/packages/dashboard-v2/src/components/utils/DragAndDrop.vue b/packages/dashboard-v2/src/components/utils/DragAndDrop.vue index ef2eef9..aebff05 100644 --- a/packages/dashboard-v2/src/components/utils/DragAndDrop.vue +++ b/packages/dashboard-v2/src/components/utils/DragAndDrop.vue @@ -131,10 +131,14 @@ export default { let uploadCount = 0 for (const [folder, files] of Object.entries(folders)) { let targetFolder = this.selectedFolder + folder - if (targetFolder.slice(-1) !== '/') { + if (targetFolder.length > 0 && targetFolder.slice(-1) !== '/') { targetFolder += '/' } + if(targetFolder === '/' || targetFolder === ROOT_FOLDER) { + targetFolder = '' + } + for (const file of files) { uploadCount += 1 const key = targetFolder + file.name diff --git a/packages/dashboard-v2/src/pages/email/EmailFolderPage.vue b/packages/dashboard-v2/src/pages/email/EmailFolderPage.vue index 06c37be..adf777b 100644 --- a/packages/dashboard-v2/src/pages/email/EmailFolderPage.vue +++ b/packages/dashboard-v2/src/pages/email/EmailFolderPage.vue @@ -1,75 +1,90 @@ @@ -78,116 +93,196 @@ import { defineComponent } from "vue"; import { api } from "boot/axios"; import { useMainStore } from "stores/main-store"; -import { decode, encode, timeSince } from "../../appUtils"; +import { apiHandler, encode, timeSince } from "../../appUtils"; export default defineComponent({ - name: 'EmailFolderPage', - data: function () { + name: "EmailFolderPage", + data: function() { return { + indexCursors: null, loading: false, + loadMoreAutomatically: true, + hasMorePages: true, rows: [], columns: [ { - name: 'sender', + name: "sender", required: true, - field: 'sender', - align: 'left', + field: "sender", + align: "left", sortable: false }, { - name: 'subject', + name: "subject", required: true, - field: 'subject', - align: 'left', + field: "subject", + align: "left", sortable: false }, { - name: 'has_attachments', + name: "has_attachments", required: true, - align: 'left', - field: 'has_attachments', - sortable: false, + align: "left", + field: "has_attachments", + sortable: false }, { - name: 'lastModified', + name: "lastModified", required: true, - align: 'left', - field: 'lastModified', - sortable: false, - }, + align: "left", + field: "lastModified", + sortable: false + } ] - } + }; }, computed: { - selectedBucket: function () { - return this.$route.params.bucket + selectedBucket: function() { + return this.$route.params.bucket; }, + selectedFolder: function() { + return "inbox"; + } }, watch: { selectedBucket(newVal) { - this.fetchFiles() - }, + this.fetchFiles(); + } }, methods: { rowClick: function(evt, row, index) { - const file = row.key.replace(/^.*[\\/]/, '') + const file = row.key.replace(/^.*[\\/]/, ""); // const folder = row.key.replace(file, '') - this.$router.push({ name: `email-file`, params: { bucket: this.selectedBucket, folder: 'inbox', file: encode(file) }}) + this.$router.push({ + name: `email-file`, + params: { bucket: this.selectedBucket, folder: this.selectedFolder, file: encode(file) } + }); }, - fetchFiles: async function () { - const self = this - this.loading = true - let truncated = true - let cursor = null - let contentFiles = [] + createOrUpdateIndex: async function(currentIndex) { + let truncated = true; - while (truncated) { - const response = await api.get(`/buckets/${this.selectedBucket}?include=customMetadata&include=httpMetadata`, { - params: { - delimiter: '/', - prefix: encode('.r2-explorer/emails/inbox/'), - cursor: cursor - } - }) + // fallback when there's no previous index + let cursor = null; + let pageNum = 0; + let indexData = { + version: 1, + cursors: [] + }; - truncated = response.data.truncated - cursor = response.data.cursor - - if (response.data.objects) { - const files = response.data.objects.filter(function(obj) { - return !obj.key.endsWith('/') // Remove selected folder - }).map(function(obj) { - const date = new Date(obj.uploaded) - - return { - ...obj, - sender: obj.customMetadata.from_name || obj.customMetadata.from_address, - subject: obj.customMetadata.subject, - has_attachments: obj.customMetadata.has_attachments === 'true', - read: obj.customMetadata.read, - lastModified: timeSince(date), - timestamp: date.getTime(), - } - }) - - for (const f of files) { - contentFiles.push(f) - } + if (currentIndex) { + // There is a previous index + indexData = currentIndex; + if (currentIndex.cursors.length > 0) { + // Pop last page, as its going to be updated + const lastPage = currentIndex.cursors.pop(); + pageNum = lastPage.page; + cursor = lastPage.cursor; } } - this.rows = contentFiles - this.loading = false + while (truncated) { + console.log(`Updating index page ${pageNum}`); + const response = await api.get(`/buckets/${this.selectedBucket}?include=customMetadata&include=httpMetadata`, { + params: { + delimiter: "/", + prefix: encode(`.r2-explorer/emails/${this.selectedFolder}/`), + cursor: cursor + } + }); + + indexData.cursors.push({ + page: pageNum, + cursor: cursor, + items: response.data.objects.length + }); + + // update cursor for next page + cursor = response.data.cursor; + truncated = response.data.truncated; + pageNum++; + } + + return indexData; + }, + getOrCreateIndex: async function() { + const indexKey = `.r2-explorer/emails/index-${this.selectedFolder}.json`; + const fileData = await apiHandler.downloadFile(this.selectedBucket, indexKey, {}).then((obj) => obj.data).catch((obj) => null); + + const updatedIndex = await this.createOrUpdateIndex(fileData); + + const blob = new Blob([JSON.stringify(updatedIndex)], { + type: "application/json" + }); + await apiHandler.uploadObjects(blob, indexKey, this.selectedBucket); + + return updatedIndex; + }, + loadNextPage: async function(index, done) { + const page = this.indexCursors[index] + + if (page) { + await this.loadIndexPage(page) + } else { + // No more pages to load + this.loadMoreAutomatically = true + this.hasMorePages = false + } + + done() + }, + fetchFiles: async function() { + this.loading = true; + + const indexData = await this.getOrCreateIndex(); + + this.indexCursors = indexData.cursors.reverse() + + await this.$refs.infScroll.setIndex(-1) // First page is 0 + await this.$refs.infScroll.trigger() + + this.loadMoreAutomatically = false + + this.loading = false; + }, + loadIndexPage: async function(page) { + const response = await apiHandler.listObjects( + this.selectedBucket, + `.r2-explorer/emails/${this.selectedFolder}/`, + "/", + page.cursor + ); + + if (response.data.objects) { + const files = response.data.objects.filter(function(obj) { + return !obj.key.endsWith("/"); // Remove selected folder + }).map(function(obj) { + const date = new Date(obj.uploaded); + + return { + ...obj, + sender: obj.customMetadata.from_name || obj.customMetadata.from_address, + subject: obj.customMetadata.subject, + has_attachments: obj.customMetadata.has_attachments === "true", + read: obj.customMetadata.read, + lastModified: timeSince(date), + timestamp: date.getTime() + }; + }); + + for (const f of files.reverse()) { + this.rows.push(f); + } + } } }, created() { - this.fetchFiles() + this.fetchFiles(); }, - setup () { + setup() { return { mainStore: useMainStore() - } - }, -}) + }; + } +});