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] Initial implementation of archiver v2 #5832

Merged
merged 22 commits into from
Oct 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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,5 +1,5 @@
# The version of OCIS to use in pipelines that test against OCIS
OCIS_COMMITID=2a1a9aa5031ee6b4f1225b5c617e178da0819521
OCIS_COMMITID=990abd60f6af17d50f0baeb5bc0a3e44ae569efd
OCIS_BRANCH=master

# The test runner source for API tests
Expand Down
11 changes: 11 additions & 0 deletions changelog/unreleased/enhancement-download-as-archive
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Enhancement: Download as archive

We've introduced archive downloads based on whether or not an archiver capability is present. The current implementation supports the archiver v2 (a.k.a. the REVA implementation).
Archive downloads are available in two different ways:
- as action on a folder (right-click context menu or actions panel in the right sidebar)
- as batch action for all selected files
The implementation is currently limited to authenticated contexts. A public links implementation will follow soon.

https://github.com/owncloud/web/pull/5832
https://github.com/owncloud/web/issues/3913
https://github.com/owncloud/web/issues/5809
2 changes: 2 additions & 0 deletions packages/web-app-files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
"description": "ownCloud web files",
"license": "AGPL-3.0",
"devDependencies": {
"@types/semver": "^7.3.8",
"copy-to-clipboard": "^3.3.1",
"filesize": "^8.0.0",
"filter-obj": "^2.0.1",
"fuse.js": "^6.4.6",
"lodash-es": "^4.17.21",
"p-queue": "^6.6.2",
"query-string": "^6.8.3",
"semver": "^6.1.0",
"vue": "^2.6.10",
"vue-gettext": "^2.1.5",
"vue2-dropzone": "^3.6.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
</oc-button>
</template>
<oc-grid v-if="displayBulkActions" gutter="small">
<div v-if="canDownload">
<oc-button
id="download-selected-btn"
key="download-selected-btn"
variation="primary"
@click="download"
>
<oc-icon name="archive" />
<translate>Download</translate>
</oc-button>
</div>
<div v-if="canCopy">
<oc-button
id="copy-selected-btn"
Expand Down Expand Up @@ -94,6 +105,10 @@ import { checkRoute } from '../../../helpers/route'
import { shareStatus } from '../../../helpers/shareStatus'
import { triggerShareAction } from '../../../helpers/share/triggerShareAction'
import PQueue from 'p-queue'
import {
isDownloadAsArchiveAvailable,
triggerDownloadAsArchive
} from '../../../helpers/download/downloadAsArchive'

export default {
mixins: [MixinRoutes, MixinDeleteResources],
Expand All @@ -108,6 +123,40 @@ export default {
: this.$gettext('Delete')
},

canDownload() {
return this.canDownloadSingleFile || this.canDownloadAsArchive
},

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

if (this.selectedFiles.length !== 1) {
return false
}

if (!this.selectedFiles[0].canDownload()) {
return false
}

return !this.selectedFiles[0].isFolder
},

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

if (!isDownloadAsArchiveAvailable()) {
return false
}

return this.selectedFiles.filter(f => !f.canDownload()).length === 0
},

canMove() {
if (
!checkRoute(['files-personal', 'files-public-list', 'files-favorites'], this.$route.name)
Expand Down Expand Up @@ -285,7 +334,7 @@ export default {
return
}

console.log(errors)
console.error(errors)
if (newShareStatus === shareStatus.accepted) {
this.showMessage({
title: this.$ngettext(
Expand All @@ -307,6 +356,30 @@ export default {
status: 'danger'
})
}
},

async download() {
if (this.selectedFiles.length === 1 && !this.selectedFiles[0].isFolder) {
await this.downloadFile(this.selectedFiles[0])
return
}
await this.downloadAsArchive()
},

async downloadAsArchive() {
await triggerDownloadAsArchive({
fileIds: this.selectedFiles.map(r => r.fileId)
}).catch(e => {
console.error(e)
this.showMessage({
title: this.$ngettext(
'Error downloading the selected file.',
'Error downloading the selected files.',
this.selectedFiles.length
),
status: 'danger'
})
})
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ import Copy from '../../mixins/actions/copy'
import CreatePublicLink from '../../mixins/actions/createPublicLink'
import DeclineShare from '../../mixins/actions/declineShare'
import Delete from '../../mixins/actions/delete'
import Download from '../../mixins/actions/download'
import DownloadFile from '../../mixins/actions/downloadFile'
import DownloadFolder from '../../mixins/actions/downloadFolder'
import Favorite from '../../mixins/actions/favorite'
import Move from '../../mixins/actions/move'
import Navigate from '../../mixins/actions/navigate'
Expand All @@ -77,7 +78,8 @@ export default {
CreatePublicLink,
DeclineShare,
Delete,
Download,
DownloadFile,
DownloadFolder,
Favorite,
Move,
Navigate,
Expand Down Expand Up @@ -153,7 +155,8 @@ export default {

menuItems.push(
...[
...this.$_download_items,
...this.$_downloadFile_items,
...this.$_downloadFolder_items,
...this.$_createPublicLink_items,
...this.$_showShares_items,
...this.$_favorite_items.map(action => {
Expand Down
39 changes: 39 additions & 0 deletions packages/web-app-files/src/helpers/download/downloadAsArchive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
ArchiverService,
archiverService as defaultArchiverService,
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
}

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)
}
}

export const isDownloadAsArchiveAvailable = (
service: ArchiverService = defaultArchiverService
): boolean => {
return service.available
}
50 changes: 20 additions & 30 deletions packages/web-app-files/src/helpers/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { bitmaskToRole, checkPermission, permissionsBitmask } from './collaborat
import { shareTypes, userShareTypes } from './shareTypes'
import { $gettext } from '../gettext'
import { DavPermission, DavProperty } from 'web-pkg/src/constants'
import { shareStatus } from './shareStatus'

// Should we move this to ODS?
export function getFileIcon(extension) {
Expand Down Expand Up @@ -40,6 +41,7 @@ export function buildResource(resource) {
extension: isFolder ? '' : extension,
path: resource.name,
type: isFolder ? 'folder' : resource.type,
isFolder,
mdate: resource.fileInfo[DavProperty.LastModifiedDate],
size: isFolder
? resource.fileInfo[DavProperty.ContentSize]
Expand All @@ -63,8 +65,7 @@ export function buildResource(resource) {
return this.permissions.indexOf(DavPermission.FolderCreateable) >= 0
},
canDownload: function() {
// TODO: as soon as we allow folder downloads as archive we want to return `true` here without exceptions
return !isFolder
return true
},
canBeDeleted: function() {
return this.permissions.indexOf(DavPermission.Deletable) >= 0
Expand Down Expand Up @@ -168,12 +169,15 @@ export function aggregateResourceShares(
}

export function buildSharedResource(share, incomingShares = false, allowSharePerm) {
const isFolder = share.item_type === 'folder'
const resource = {
id: share.id,
fileId: share.item_source,
type: share.item_type
type: share.item_type,
isFolder,
sdate: share.stime * 1000,
indicators: []
}
const isFolder = resource.type === 'folder'

if (incomingShares) {
resource.resourceOwner = {
Expand All @@ -192,43 +196,28 @@ export function buildSharedResource(share, incomingShares = false, allowSharePer
resource.status = share.state
resource.name = path.basename(share.file_target)
resource.path = share.file_target
resource.isReceivedShare = () => true
resource.canDownload = () => share.state === shareStatus.accepted
resource.canShare = () => checkPermission(share.permissions, 'share')
resource.canRename = () => checkPermission(share.permissions, 'update')
resource.canBeDeleted = () => checkPermission(share.permissions, 'delete')
} else {
resource.sharedWith = share.sharedWith
resource.shareOwner = share.uid_owner
resource.shareOwnerDisplayname = share.displayname_owner
resource.name = path.basename(share.path)
resource.path = share.path
// permissions irrelevant here
resource.isReceivedShare = () => false
resource.canDownload = () => true
resource.canShare = () => true
resource.canRename = () => true
resource.canBeDeleted = () => true
}

resource.extension = isFolder ? '' : _getFileExtension(resource.name)
// FIXME: add actual permission parsing
resource.icon = isFolder ? 'folder' : getFileIcon(resource.extension)
resource.isReceivedShare = () => incomingShares
resource.canUpload = () => true
resource.canBeDeleted = () => {
if (!resource.isReceivedShare()) {
return true
}
return checkPermission(share.permissions, 'delete')
}
resource.canRename = () => {
if (!resource.isReceivedShare()) {
return true
}
return checkPermission(share.permissions, 'update')
}
resource.canShare = () => {
if (!resource.isReceivedShare()) {
return true
}
return checkPermission(share.permissions, 'share')
}
resource.isMounted = () => false
resource.canDownload = () => !isFolder
resource.share = buildShare(share, resource, allowSharePerm)
resource.indicators = []
resource.icon = isFolder ? 'folder' : getFileIcon(resource.extension)
resource.sdate = share.stime * 1000

return resource
}
Expand Down Expand Up @@ -347,6 +336,7 @@ export function buildDeletedResource(resource) {
const extension = isFolder ? '' : _getFileExtension(fullName)
return {
type: isFolder ? 'folder' : resource.type,
isFolder,
ddate: resource.fileInfo[DavProperty.TrashbinDeletedDate],
name: path.basename(fullName),
extension,
Expand Down
8 changes: 7 additions & 1 deletion packages/web-app-files/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import quickActionsImport from './quickActions'
import store from './store'
import { FilterSearch, SDKSearch } from './search'
import { bus } from 'web-pkg/src/instance'
import { Registry } from './services'
import { archiverService, Registry } from './services'
import fileSideBars from './fileSideBars'
import routes from './routes'
import get from 'lodash-es/get'

// just a dummy function to trick gettext tools
function $gettext(msg) {
Expand Down Expand Up @@ -131,5 +132,10 @@ export default {
// registry that does not rely on call order, aka first register "on" and only after emit.
bus.emit('app.search.register.provider', Registry.filterSearch)
bus.emit('app.search.register.provider', Registry.sdkSearch)
// initialize services
archiverService.initialize(
runtimeStore.getters.configuration.server,
get(runtimeStore, 'getters.capabilities.files.archivers', [])
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@ import { isTrashbinRoute } from '../../helpers/route'

export default {
computed: {
$_download_items() {
$_downloadFile_items() {
return [
{
icon: 'file_download',
handler: this.$_download_trigger,
handler: this.$_downloadFile_trigger,
label: () => {
return this.$gettext('Download')
},
isEnabled: ({ resource }) => {
if (isTrashbinRoute(this.$route)) {
return false
}

if (resource.isFolder) {
return false
}
return resource.canDownload()
},
canBeDefault: true,
componentType: 'oc-button',
class: 'oc-files-actions-download-trigger'
class: 'oc-files-actions-download-file-trigger'
}
]
}
},
methods: {
$_download_trigger(resource) {
$_downloadFile_trigger(resource) {
this.downloadFile(resource)
}
}
Expand Down
Loading