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

[full-ci] feat(public-links): archive downloads in public links #5924

Merged
merged 17 commits into from
Nov 8, 2021
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
2 changes: 1 addition & 1 deletion .drone.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# The version of OCIS to use in pipelines that test against OCIS
OCIS_COMMITID=5aeb76f6c8aee574fdd2710f9e883efe0c8166f8
OCIS_COMMITID=25d6e4efd119049f290683d6d126185368058b4e
OCIS_BRANCH=master
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Summary
* Bugfix - Fix location picker breadcrumb url encoding: [#5940](https://github.com/owncloud/web/pull/5940)
* Bugfix - Context menu rendering: [#5952](https://github.com/owncloud/web/pull/5952)
* Enhancement - Default action order: [#5952](https://github.com/owncloud/web/pull/5952)
* Enhancement - Reduced sidebar width: [#5981](https://github.com/owncloud/web/issues/5981)

Details
-------
Expand Down Expand Up @@ -40,6 +41,14 @@ Details

https://github.com/owncloud/web/pull/5952

* Enhancement - Reduced sidebar width: [#5981](https://github.com/owncloud/web/issues/5981)

We reduced the sidebar width to give the files list more horizontal room, especially on medium
sized screens.

https://github.com/owncloud/web/issues/5981
https://github.com/owncloud/web/pull/5983

Changelog for ownCloud Web [4.4.0] (2021-10-26)
=======================================
The following sections list the changes in ownCloud web 4.4.0 relevant to
Expand Down
5 changes: 5 additions & 0 deletions changelog/unreleased/enhancement-chunks-folder
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Rename `_chunks` folder to `chunks`

We've renamed the `_chunks` folder to `chunks` in the ownCloud Web build output in order to make it more easily embedable with the Go embed directive.

https://github.com/owncloud/web/pull/5988
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-files-sidebar-reduced-width
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Reduced sidebar width

We reduced the sidebar width to give the files list more horizontal room, especially on medium sized screens.

https://github.com/owncloud/web/issues/5981
https://github.com/owncloud/web/pull/5983
10 changes: 10 additions & 0 deletions changelog/unreleased/enhancement-public-link-capabilities
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Enhancement: App provider and archiver on public links

We made the app provider and archiver services available on public links. As a prerequisite for this we needed to make backend capabilities available on public links, which will
be beneficial for all future extension development.

https://github.com/owncloud/web/pull/5924
https://github.com/owncloud/web/issues/5884
https://github.com/owncloud/ocis/issues/2479
https://github.com/owncloud/web/issues/2479
https://github.com/owncloud/web/issues/5901
63 changes: 36 additions & 27 deletions packages/web-app-external/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ import { mapGetters } from 'vuex'
import ErrorScreen from './components/ErrorScreen.vue'
import LoadingScreen from './components/LoadingScreen.vue'

// FIXME: hacky, get rid asap, just a workaround
// same as packages/web-app-files/src/views/PublicFiles.vue
const unauthenticatedUserReady = async (router, store) => {
if (store.getters.userReady) {
return
}

const publicToken = router.currentRoute.query['public-token']
const publicLinkPassword = store.getters['Files/publicLinkPassword']

await store.dispatch('loadCapabilities', {
publicToken,
...(publicLinkPassword && { user: 'public', password: publicLinkPassword })
})

store.commit('SET_USER_READY', true)
}

export default {
name: 'ExternalApp',

Expand All @@ -48,6 +66,7 @@ export default {
}),
computed: {
...mapGetters(['getToken', 'capabilities', 'configuration']),
...mapGetters('Files', ['publicLinkPassword']),

pageTitle() {
const translated = this.$gettext('"%{appName}" app page')
Expand All @@ -69,37 +88,27 @@ export default {
}
},
async created() {
this.loading = true

// TODO: Enable externalApp usage on public routes below
// initialize headers()
await unauthenticatedUserReady(this.$router, this.$store)

// if (this.isPublicRoute) {
// // send auth header here if public route
// // if password exists send it via basicauth public:password

// // headers.append('public-token', 'uUCPJghnVUspjxe')
// // const password = this.publicLinkPassword

// // if (password) {
// // headers.append( Authorization: 'Basic ' + Buffer.from('public:' + password).toString('base64') }
// // }
// } else {
// - check for token
// - abort if falsy
// - build headers as below
// }
this.loading = true

if (!this.getToken) {
this.loading = false
this.loadingError = true
return
// build headers with respect to the actual auth situation
const { 'public-token': publicToken } = this.$route.query
const publicLinkPassword = this.publicLinkPassword
const accessToken = this.getToken
const headers = {
'X-Requested-With': 'XMLHttpRequest',
...(publicToken && { 'public-token': publicToken }),
...(publicLinkPassword && {
Authorization:
'Basic ' + Buffer.from(['public', publicLinkPassword].join(':')).toString('base64')
}),
...(accessToken && {
Authorization: 'Bearer ' + accessToken
})
}

const headers = new Headers()
headers.append('Authorization', 'Bearer ' + this.getToken)
headers.append('X-Requested-With', 'XMLHttpRequest')

// fetch iframe params for app and file
const configUrl = this.configuration.server
const appOpenUrl = this.capabilities.files.app_providers[0].open_url.replace('/app', 'app')
const url = configUrl + appOpenUrl + '?file_id=' + this.fileId + '&app_name=' + this.appName
Expand Down
7 changes: 4 additions & 3 deletions packages/web-app-external/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const routes = [
app: App
},
meta: {
title: $gettext('External app')
title: $gettext('External app'),
auth: false
}
}
]
Expand All @@ -30,7 +31,7 @@ export default {
routes,
store,
translations,
async ready({ store: runtimeStore }) {
await runtimeStore.dispatch('External/fetchMimeTypes')
userReady({ store }) {
store.dispatch('External/fetchMimeTypes')
}
}
11 changes: 8 additions & 3 deletions packages/web-app-external/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ const actions = {
throw new Error('Error fetching app provider MIME types')
}

const mimeTypes = await response.json()
commit('SET_MIME_TYPES', mimeTypes['mime-types'])
const { 'mime-types': mimeTypes } = await response.json()

commit('SET_MIME_TYPES', mimeTypes)
}
}

const getters = {
getMimeTypes: (state: typeof State): { [key: string]: string } => {
mimeTypes: (
state: typeof State
): {
[key: string]: string
} => {
return state.mimeTypes
}
}
Expand Down
13 changes: 9 additions & 4 deletions packages/web-app-external/tests/unit/app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const componentStubs = {
}

const $route = {
query: {
'public-token': 'a-token'
},
params: {
app: 'exampleApp',
file_id: '2147491323'
Expand All @@ -30,6 +33,7 @@ const storeOptions = {
configuration: jest.fn(() => ({
server: 'http://example.com/'
})),
userReady: () => true,
capabilities: jest.fn(() => ({
files: {
app_providers: [
Expand All @@ -46,7 +50,7 @@ const storeOptions = {
External: {
namespaced: true,
getters: {
getMimeTypes: jest.fn()
mimeTypes: jest.fn()
},
actions: {
fetchMimeTypes: jest.fn()
Expand Down Expand Up @@ -84,7 +88,7 @@ describe('The app provider extension', () => {
fetchMock.resetMocks()
})

it('should show a loading spinner while loading', () => {
it('should show a loading spinner while loading', async () => {
global.fetch = jest.fn(() =>
setTimeout(() => {
Promise.resolve({
Expand All @@ -94,19 +98,21 @@ describe('The app provider extension', () => {
}, 500)
)
const wrapper = createShallowMountWrapper()

await wrapper.vm.$nextTick()
expect(wrapper).toMatchSnapshot()
})
it('should show a meaningful message if an error occurs during loading', async () => {
fetchMock.mockReject(new Error('fake error message'))
const wrapper = createShallowMountWrapper()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper).toMatchSnapshot()
})
it('should fail for unauthenticated users', async () => {
fetchMock.mockResponseOnce({ status: 401 })
const wrapper = createShallowMountWrapper()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper).toMatchSnapshot()
})
it('should be able to load an iFrame via get', async () => {
Expand All @@ -131,7 +137,6 @@ describe('The app provider extension', () => {
json: () => providerSuccessResponsePost
})
)

const wrapper = createShallowMountWrapper()
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
Expand Down
2 changes: 1 addition & 1 deletion packages/web-app-files/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
id="files-sidebar"
ref="filesSidebar"
tabindex="-1"
class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl"
class="uk-width-1-1 uk-width-1-3@m uk-width-1-4@xl"
@beforeDestroy="focusSideBar"
@mounted="focusSideBar"
@fileChanged="focusSideBar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import MixinRoutes from '../../../mixins/routes'
import MixinDeleteResources from '../../../mixins/deleteResources'
import { cloneStateObject } from '../../../helpers/store'
import { canBeMoved } from '../../../helpers/permissions'
import { checkRoute } from '../../../helpers/route'
import { checkRoute, isPublicFilesRoute } from '../../../helpers/route'
import { shareStatus } from '../../../helpers/shareStatus'
import { triggerShareAction } from '../../../helpers/share/triggerShareAction'
import PQueue from 'p-queue'
Expand Down Expand Up @@ -129,7 +129,7 @@ export default {

canDownloadSingleFile() {
if (
!checkRoute(['files-personal', 'files-favorites', 'files-public-list'], this.$route.name)
!checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name)
) {
return false
}
Expand All @@ -146,7 +146,9 @@ export default {
},

canDownloadAsArchive() {
if (!checkRoute(['files-personal', 'files-favorites'], this.$route.name)) {
if (
!checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name)
) {
return false
}

Expand Down Expand Up @@ -363,12 +365,16 @@ export default {
await this.downloadFile(this.selectedFiles[0])
return
}

await this.downloadAsArchive()
},

async downloadAsArchive() {
await triggerDownloadAsArchive({
fileIds: this.selectedFiles.map((r) => r.fileId)
fileIds: this.selectedFiles.map((r) => r.fileId),
...(isPublicFilesRoute(this.$route) && {
publicToken: this.$route.params.item.split('/')[0]
})
}).catch((e) => {
console.error(e)
this.showMessage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export default {
computed: {
...mapGetters('Files', ['highlightedFile', 'currentFolder']),

appList() {
return this.$_fileActions_loadApps(this.highlightedFile) || []
},

actions() {
return this.$_fileActions_getAllAvailableActions(this.highlightedFile)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/web-app-files/src/components/SideBar/FileInfo.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="file_info">
<oc-icon :name="file.icon" size="large" class="file_info__icon" />
<div class="file_info__body">
<div class="file_info__body oc-text-overflow">
<h3 tabindex="-1">
<oc-resource-name
:name="file.name"
Expand Down
21 changes: 17 additions & 4 deletions packages/web-app-files/src/helpers/download/downloadAsArchive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,45 @@ import {
ClientService,
clientService as defaultClientService
} from '../../services'

import { major } from 'semver'
import { RuntimeError } from 'web-runtime/src/container/error'

interface TriggerDownloadAsArchiveOptions {
fileIds: string[]
archiverService?: ArchiverService
clientService?: ClientService
publicToken?: string
}

export const triggerDownloadAsArchive = async (
options: TriggerDownloadAsArchiveOptions
): Promise<void> => {
const archiverService = options.archiverService || defaultArchiverService
const clientService = options.clientService || defaultClientService

if (!isDownloadAsArchiveAvailable(archiverService)) {
throw new RuntimeError('no archiver capability available')
}

if (options.fileIds.length === 0) {
throw new RuntimeError('requested archive with empty list of resources')
}

const majorVersion = major(archiverService.capability.version)
if (majorVersion === 2) {
const queryParams = [...options.fileIds.map((id) => `id=${id}`)]
const archiverUrl = archiverService.url + '?' + queryParams.join('&')
window.location = await clientService.owncloudSdk.signUrl(archiverUrl)
if (majorVersion !== 2) {
return
}

const queryParams = [
options.publicToken ? `public-token=${options.publicToken}` : '',
...options.fileIds.map((id) => `id=${id}`)
].filter(Boolean)
const archiverUrl = archiverService.url + '?' + queryParams.join('&')

window.location = options.publicToken
? (archiverUrl as any)
: await clientService.owncloudSdk.signUrl(archiverUrl)
}

export const isDownloadAsArchiveAvailable = (
Expand Down
Loading