Consistent naming to Share Link Bundle

This commit is contained in:
shamoon 2025-11-05 17:24:23 -08:00
parent 4db5cf0ee8
commit 1ccdd1f4db
No known key found for this signature in database
24 changed files with 283 additions and 271 deletions

View File

@ -1,7 +0,0 @@
describe('ShareBundleDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@ -1,7 +0,0 @@
describe('ShareBundleManageDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareBundleManageDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@ -50,9 +50,9 @@
} @else { } @else {
<div class="d-flex flex-column gap-3"> <div class="d-flex flex-column gap-3">
<div class="alert alert-success mb-0" role="status"> <div class="alert alert-success mb-0" role="status">
<h6 class="alert-heading mb-1" i18n>Share link requested</h6> <h6 class="alert-heading mb-1" i18n>Share link bundle requested</h6>
<p class="mb-0 small" i18n> <p class="mb-0 small" i18n>
You can copy the link below or open the management screen to monitor its progress. The link will start working once it is ready. You can copy the share link below or open the manager to monitor progress. The link will start working once the bundle is ready.
</p> </p>
</div> </div>
<dl class="row mb-0 small"> <dl class="row mb-0 small">
@ -105,11 +105,11 @@
<div class="modal-footer"> <div class="modal-footer">
<div class="d-flex align-items-center gap-2 w-100"> <div class="d-flex align-items-center gap-2 w-100">
<div class="text-light fst-italic small"> <div class="text-light fst-italic small">
<ng-container i18n>A zip file containing the selected documents will be created. This process happens in the background and may take some time, especially for large bundles.</ng-container> <ng-container i18n>A zip file containing the selected documents will be created for this share link bundle. This process happens in the background and may take some time, especially for large bundles.</ng-container>
</div> </div>
<button type="button" class="btn btn-outline-secondary btn-sm ms-auto" (click)="cancel()">{{ cancelBtnCaption }}</button> <button type="button" class="btn btn-outline-secondary btn-sm ms-auto" (click)="cancel()">{{ cancelBtnCaption }}</button>
@if (createdBundle) { @if (createdBundle) {
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="openManage()" i18n>Open manage links</button> <button type="button" class="btn btn-outline-secondary btn-sm" (click)="openManage()" i18n>Manage share link bundles</button>
} }
@if (!createdBundle) { @if (!createdBundle) {

View File

@ -0,0 +1,7 @@
describe('ShareLinkBundleDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareLinkBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@ -4,17 +4,17 @@ import { Component, Input, inject } from '@angular/core'
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms' import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import {
SHARE_BUNDLE_FILE_VERSION_LABELS,
SHARE_BUNDLE_STATUS_LABELS,
ShareBundleCreatePayload,
ShareBundleStatus,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import { import {
FileVersion, FileVersion,
SHARE_LINK_EXPIRATION_OPTIONS, SHARE_LINK_EXPIRATION_OPTIONS,
} from 'src/app/data/share-link' } from 'src/app/data/share-link'
import {
SHARE_LINK_BUNDLE_FILE_VERSION_LABELS,
SHARE_LINK_BUNDLE_STATUS_LABELS,
ShareLinkBundleCreatePayload,
ShareLinkBundleStatus,
ShareLinkBundleSummary,
} from 'src/app/data/share-link-bundle'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe' import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
@ -22,8 +22,8 @@ import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
@Component({ @Component({
selector: 'pngx-share-bundle-dialog', selector: 'pngx-share-link-bundle-dialog',
templateUrl: './share-bundle-dialog.component.html', templateUrl: './share-link-bundle-dialog.component.html',
imports: [ imports: [
CommonModule, CommonModule,
ReactiveFormsModule, ReactiveFormsModule,
@ -33,7 +33,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.compone
], ],
providers: [], providers: [],
}) })
export class ShareBundleDialogComponent extends ConfirmDialogComponent { export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
private formBuilder = inject(FormBuilder) private formBuilder = inject(FormBuilder)
private clipboard = inject(Clipboard) private clipboard = inject(Clipboard)
private toastService = inject(ToastService) private toastService = inject(ToastService)
@ -46,20 +46,20 @@ export class ShareBundleDialogComponent extends ConfirmDialogComponent {
shareArchiveVersion: [true], shareArchiveVersion: [true],
expirationDays: [7], expirationDays: [7],
}) })
payload: ShareBundleCreatePayload | null = null payload: ShareLinkBundleCreatePayload | null = null
readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS
createdBundle: ShareBundleSummary | null = null createdBundle: ShareLinkBundleSummary | null = null
copied = false copied = false
onOpenManage?: () => void onOpenManage?: () => void
readonly statuses = ShareBundleStatus readonly statuses = ShareLinkBundleStatus
constructor() { constructor() {
super() super()
this.loading = false this.loading = false
this.title = $localize`Share Selected Documents` this.title = $localize`Create share link bundle`
this.btnCaption = $localize`Create` this.btnCaption = $localize`Create link`
} }
@Input() @Input()
@ -82,14 +82,14 @@ export class ShareBundleDialogComponent extends ConfirmDialogComponent {
super.confirm() super.confirm()
} }
getShareUrl(bundle: ShareBundleSummary): string { getShareUrl(bundle: ShareLinkBundleSummary): string {
const apiURL = new URL(environment.apiBaseUrl) const apiURL = new URL(environment.apiBaseUrl)
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${ return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
bundle.slug bundle.slug
}` }`
} }
copy(bundle: ShareBundleSummary): void { copy(bundle: ShareLinkBundleSummary): void {
const success = this.clipboard.copy(this.getShareUrl(bundle)) const success = this.clipboard.copy(this.getShareUrl(bundle))
if (success) { if (success) {
this.copied = true this.copied = true
@ -108,11 +108,11 @@ export class ShareBundleDialogComponent extends ConfirmDialogComponent {
} }
} }
statusLabel(status: ShareBundleSummary['status']): string { statusLabel(status: ShareLinkBundleSummary['status']): string {
return SHARE_BUNDLE_STATUS_LABELS[status] ?? status return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status
} }
fileVersionLabel(version: FileVersion): string { fileVersionLabel(version: FileVersion): string {
return SHARE_BUNDLE_FILE_VERSION_LABELS[version] ?? version return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version
} }
} }

View File

@ -7,7 +7,7 @@
@if (loading) { @if (loading) {
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<div class="spinner-border spinner-border-sm" role="status"></div> <div class="spinner-border spinner-border-sm" role="status"></div>
<span i18n>Loading bulk share links…</span> <span i18n>Loading share link bundles…</span>
</div> </div>
} }
@if (!loading && error) { @if (!loading && error) {
@ -22,7 +22,7 @@
</p> </p>
</div> </div>
@if (bundles.length === 0) { @if (bundles.length === 0) {
<p class="mb-0 text-muted fst-italic" i18n>No bulk share links currently exist.</p> <p class="mb-0 text-muted fst-italic" i18n>No share link bundles currently exist.</p>
} }
@if (bundles.length > 0) { @if (bundles.length > 0) {
<div class="table-responsive"> <div class="table-responsive">
@ -92,7 +92,7 @@
@if (copiedSlug !== bundle.slug) { @if (copiedSlug !== bundle.slug) {
<i-bs name="clipboard"></i-bs> <i-bs name="clipboard"></i-bs>
} }
<span class="visually-hidden" i18n>Copy link</span> <span class="visually-hidden" i18n>Copy share link</span>
</button> </button>
@if (bundle.status === statuses.Failed) { @if (bundle.status === statuses.Failed) {
<button <button
@ -112,7 +112,7 @@
(click)="delete(bundle)" (click)="delete(bundle)"
> >
<i-bs name="trash"></i-bs> <i-bs name="trash"></i-bs>
<span class="visually-hidden" i18n>Delete link</span> <span class="visually-hidden" i18n>Delete share link bundle</span>
</button> </button>
</div> </div>
</td> </td>

View File

@ -0,0 +1,7 @@
describe('ShareLinkBundleManageDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareLinkBundleManageDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@ -4,40 +4,40 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs' import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
import {
SHARE_BUNDLE_FILE_VERSION_LABELS,
SHARE_BUNDLE_STATUS_LABELS,
ShareBundleStatus,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import { FileVersion } from 'src/app/data/share-link' import { FileVersion } from 'src/app/data/share-link'
import {
SHARE_LINK_BUNDLE_FILE_VERSION_LABELS,
SHARE_LINK_BUNDLE_STATUS_LABELS,
ShareLinkBundleStatus,
ShareLinkBundleSummary,
} from 'src/app/data/share-link-bundle'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe' import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service' import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({ @Component({
selector: 'pngx-share-bundle-manage-dialog', selector: 'pngx-share-link-bundle-manage-dialog',
templateUrl: './share-bundle-manage-dialog.component.html', templateUrl: './share-link-bundle-manage-dialog.component.html',
imports: [CommonModule, NgxBootstrapIconsModule, FileSizePipe], imports: [CommonModule, NgxBootstrapIconsModule, FileSizePipe],
}) })
export class ShareBundleManageDialogComponent export class ShareLinkBundleManageDialogComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
private activeModal = inject(NgbActiveModal) private activeModal = inject(NgbActiveModal)
private shareBundleService = inject(ShareBundleService) private shareLinkBundleService = inject(ShareLinkBundleService)
private toastService = inject(ToastService) private toastService = inject(ToastService)
private clipboard = inject(Clipboard) private clipboard = inject(Clipboard)
title = $localize`Bulk Share Links` title = $localize`Share link bundles`
bundles: ShareBundleSummary[] = [] bundles: ShareLinkBundleSummary[] = []
error: string | null = null error: string | null = null
copiedSlug: string | null = null copiedSlug: string | null = null
readonly statuses = ShareBundleStatus readonly statuses = ShareLinkBundleStatus
readonly fileVersions = FileVersion readonly fileVersions = FileVersion
private readonly refresh$ = new Subject<boolean>() private readonly refresh$ = new Subject<boolean>()
@ -50,14 +50,14 @@ export class ShareBundleManageDialogComponent
this.loading = true this.loading = true
} }
this.error = null this.error = null
return this.shareBundleService.listAllBundles().pipe( return this.shareLinkBundleService.listAllBundles().pipe(
catchError((error) => { catchError((error) => {
if (!silent) { if (!silent) {
this.loading = false this.loading = false
} }
this.error = $localize`Failed to load bulk share links.` this.error = $localize`Failed to load share link bundles.`
this.toastService.showError( this.toastService.showError(
$localize`Error retrieving bulk share links.`, $localize`Error retrieving share link bundles.`,
error error
) )
return of(null) return of(null)
@ -84,15 +84,15 @@ export class ShareBundleManageDialogComponent
super.ngOnDestroy() super.ngOnDestroy()
} }
getShareUrl(bundle: ShareBundleSummary): string { getShareUrl(bundle: ShareLinkBundleSummary): string {
const apiURL = new URL(environment.apiBaseUrl) const apiURL = new URL(environment.apiBaseUrl)
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${ return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
bundle.slug bundle.slug
}` }`
} }
copy(bundle: ShareBundleSummary): void { copy(bundle: ShareLinkBundleSummary): void {
if (bundle.status !== ShareBundleStatus.Ready) { if (bundle.status !== ShareLinkBundleStatus.Ready) {
return return
} }
const success = this.clipboard.copy(this.getShareUrl(bundle)) const success = this.clipboard.copy(this.getShareUrl(bundle))
@ -105,30 +105,30 @@ export class ShareBundleManageDialogComponent
} }
} }
delete(bundle: ShareBundleSummary): void { delete(bundle: ShareLinkBundleSummary): void {
this.error = null this.error = null
this.loading = true this.loading = true
this.shareBundleService.delete(bundle).subscribe({ this.shareLinkBundleService.delete(bundle).subscribe({
next: () => { next: () => {
this.toastService.showInfo($localize`Bulk share link deleted.`) this.toastService.showInfo($localize`Share link bundle deleted.`)
this.triggerRefresh(false) this.triggerRefresh(false)
}, },
error: (e) => { error: (e) => {
this.loading = false this.loading = false
this.toastService.showError( this.toastService.showError(
$localize`Error deleting bulk share link.`, $localize`Error deleting share link bundle.`,
e e
) )
}, },
}) })
} }
retry(bundle: ShareBundleSummary): void { retry(bundle: ShareLinkBundleSummary): void {
this.error = null this.error = null
this.shareBundleService.rebuildBundle(bundle.id).subscribe({ this.shareLinkBundleService.rebuildBundle(bundle.id).subscribe({
next: (updated) => { next: (updated) => {
this.toastService.showInfo( this.toastService.showInfo(
$localize`Bulk share link rebuild requested.` $localize`Share link bundle rebuild requested.`
) )
this.replaceBundle(updated) this.replaceBundle(updated)
}, },
@ -138,19 +138,19 @@ export class ShareBundleManageDialogComponent
}) })
} }
statusLabel(status: ShareBundleStatus): string { statusLabel(status: ShareLinkBundleStatus): string {
return SHARE_BUNDLE_STATUS_LABELS[status] ?? status return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status
} }
fileVersionLabel(version: FileVersion): string { fileVersionLabel(version: FileVersion): string {
return SHARE_BUNDLE_FILE_VERSION_LABELS[version] ?? version return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version
} }
close(): void { close(): void {
this.activeModal.close() this.activeModal.close()
} }
private replaceBundle(updated: ShareBundleSummary): void { private replaceBundle(updated: ShareLinkBundleSummary): void {
const index = this.bundles.findIndex((bundle) => bundle.id === updated.id) const index = this.bundles.findIndex((bundle) => bundle.id === updated.id)
if (index >= 0) { if (index >= 0) {
this.bundles = [ this.bundles = [

View File

@ -112,11 +112,11 @@
</div> </div>
</button> </button>
<div ngbDropdownMenu aria-labelledby="dropdownSend" class="shadow"> <div ngbDropdownMenu aria-labelledby="dropdownSend" class="shadow">
<button ngbDropdownItem (click)="shareSelected()"> <button ngbDropdownItem (click)="createShareLinkBundle()">
<i-bs name="link"></i-bs>&nbsp;<ng-container i18n>Create a share link</ng-container> <i-bs name="link"></i-bs>&nbsp;<ng-container i18n>Create a share link bundle</ng-container>
</button> </button>
<button ngbDropdownItem (click)="manageShareLinks()"> <button ngbDropdownItem (click)="manageShareLinkBundles()">
<i-bs name="list-ul"></i-bs>&nbsp;<ng-container i18n>Manage existing links</ng-container> <i-bs name="list-ul"></i-bs>&nbsp;<ng-container i18n>Manage share link bundles</ng-container>
</button> </button>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@if (emailEnabled) { @if (emailEnabled) {

View File

@ -33,7 +33,7 @@ import {
SelectionDataItem, SelectionDataItem,
} from 'src/app/services/rest/document.service' } from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service' import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service' import { TagService } from 'src/app/services/rest/tag.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
@ -55,8 +55,8 @@ import {
} from '../../common/filterable-dropdown/filterable-dropdown.component' } from '../../common/filterable-dropdown/filterable-dropdown.component'
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component' import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
import { ShareBundleDialogComponent } from '../../common/share-bundle-dialog/share-bundle-dialog.component' import { ShareLinkBundleDialogComponent } from '../../common/share-link-bundle-dialog/share-link-bundle-dialog.component'
import { ShareBundleManageDialogComponent } from '../../common/share-bundle-manage-dialog/share-bundle-manage-dialog.component' import { ShareLinkBundleManageDialogComponent } from '../../common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component' import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
@ -90,7 +90,7 @@ export class BulkEditorComponent
private customFieldService = inject(CustomFieldsService) private customFieldService = inject(CustomFieldsService)
private permissionService = inject(PermissionsService) private permissionService = inject(PermissionsService)
private savedViewService = inject(SavedViewService) private savedViewService = inject(SavedViewService)
private shareBundleService = inject(ShareBundleService) private shareLinkBundleService = inject(ShareLinkBundleService)
tagSelectionModel = new FilterableDropdownSelectionModel(true) tagSelectionModel = new FilterableDropdownSelectionModel(true)
correspondentSelectionModel = new FilterableDropdownSelectionModel() correspondentSelectionModel = new FilterableDropdownSelectionModel()
@ -912,12 +912,12 @@ export class BulkEditorComponent
return this.settings.get(SETTINGS_KEYS.EMAIL_ENABLED) return this.settings.get(SETTINGS_KEYS.EMAIL_ENABLED)
} }
shareSelected() { createShareLinkBundle() {
const modal = this.modalService.open(ShareBundleDialogComponent, { const modal = this.modalService.open(ShareLinkBundleDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'lg', size: 'lg',
}) })
const dialog = modal.componentInstance as ShareBundleDialogComponent const dialog = modal.componentInstance as ShareLinkBundleDialogComponent
const selectedDocuments = this.list.documents.filter((d) => const selectedDocuments = this.list.documents.filter((d) =>
this.list.selected.has(d.id) this.list.selected.has(d.id)
) )
@ -931,7 +931,7 @@ export class BulkEditorComponent
} }
dialog.loading = true dialog.loading = true
dialog.buttonsEnabled = false dialog.buttonsEnabled = false
this.shareBundleService this.shareLinkBundleService
.createBundle(payload) .createBundle(payload)
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
@ -943,17 +943,17 @@ export class BulkEditorComponent
dialog.payload = null dialog.payload = null
dialog.onOpenManage = () => { dialog.onOpenManage = () => {
modal.close() modal.close()
this.manageShareLinks() this.manageShareLinkBundles()
} }
this.toastService.showInfo( this.toastService.showInfo(
$localize`Bulk share link creation requested.` $localize`Share link bundle creation requested.`
) )
}, },
error: (error) => { error: (error) => {
dialog.loading = false dialog.loading = false
dialog.buttonsEnabled = true dialog.buttonsEnabled = true
this.toastService.showError( this.toastService.showError(
$localize`Bulk share link creation is not available yet.`, $localize`Share link bundle creation is not available yet.`,
error error
) )
}, },
@ -961,8 +961,8 @@ export class BulkEditorComponent
}) })
} }
manageShareLinks() { manageShareLinkBundles() {
const modal = this.modalService.open(ShareBundleManageDialogComponent, { const modal = this.modalService.open(ShareLinkBundleManageDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'lg', size: 'lg',
}) })

View File

@ -1,40 +0,0 @@
import { FileVersion } from './share-link'
export enum ShareBundleStatus {
Pending = 'pending',
Processing = 'processing',
Ready = 'ready',
Failed = 'failed',
}
export interface ShareBundleSummary {
id: number
slug: string
created: string // Date
expiration?: string // Date
documents: number[]
document_count: number
file_version: FileVersion
status: ShareBundleStatus
built_at?: string
size_bytes?: number
last_error?: string
}
export interface ShareBundleCreatePayload {
document_ids: number[]
file_version: FileVersion
expiration_days: number | null
}
export const SHARE_BUNDLE_STATUS_LABELS: Record<ShareBundleStatus, string> = {
[ShareBundleStatus.Pending]: $localize`Pending`,
[ShareBundleStatus.Processing]: $localize`Processing`,
[ShareBundleStatus.Ready]: $localize`Ready`,
[ShareBundleStatus.Failed]: $localize`Failed`,
}
export const SHARE_BUNDLE_FILE_VERSION_LABELS: Record<FileVersion, string> = {
[FileVersion.Archive]: $localize`Archive`,
[FileVersion.Original]: $localize`Original`,
}

View File

@ -0,0 +1,46 @@
import { FileVersion } from './share-link'
export enum ShareLinkBundleStatus {
Pending = 'pending',
Processing = 'processing',
Ready = 'ready',
Failed = 'failed',
}
export interface ShareLinkBundleSummary {
id: number
slug: string
created: string // Date
expiration?: string // Date
documents: number[]
document_count: number
file_version: FileVersion
status: ShareLinkBundleStatus
built_at?: string
size_bytes?: number
last_error?: string
}
export interface ShareLinkBundleCreatePayload {
document_ids: number[]
file_version: FileVersion
expiration_days: number | null
}
export const SHARE_LINK_BUNDLE_STATUS_LABELS: Record<
ShareLinkBundleStatus,
string
> = {
[ShareLinkBundleStatus.Pending]: $localize`Pending`,
[ShareLinkBundleStatus.Processing]: $localize`Processing`,
[ShareLinkBundleStatus.Ready]: $localize`Ready`,
[ShareLinkBundleStatus.Failed]: $localize`Failed`,
}
export const SHARE_LINK_BUNDLE_FILE_VERSION_LABELS: Record<
FileVersion,
string
> = {
[FileVersion.Archive]: $localize`Archive`,
[FileVersion.Original]: $localize`Original`,
}

View File

@ -1,38 +0,0 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import {
ShareBundleCreatePayload,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class ShareBundleService extends AbstractNameFilterService<ShareBundleSummary> {
constructor() {
super()
this.resourceName = 'share_bundles'
}
createBundle(
payload: ShareBundleCreatePayload
): Observable<ShareBundleSummary> {
this.clearCache()
return this.http.post<ShareBundleSummary>(this.getResourceUrl(), payload)
}
rebuildBundle(bundleId: number): Observable<ShareBundleSummary> {
this.clearCache()
return this.http.post<ShareBundleSummary>(
this.getResourceUrl(bundleId, 'rebuild'),
{}
)
}
listAllBundles(): Observable<ShareBundleSummary[]> {
return this.list(1, 1000, 'created', true).pipe(
map((response) => response.results)
)
}
}

View File

@ -0,0 +1,41 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import {
ShareLinkBundleCreatePayload,
ShareLinkBundleSummary,
} from 'src/app/data/share-link-bundle'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class ShareLinkBundleService extends AbstractNameFilterService<ShareLinkBundleSummary> {
constructor() {
super()
this.resourceName = 'share_link_bundles'
}
createBundle(
payload: ShareLinkBundleCreatePayload
): Observable<ShareLinkBundleSummary> {
this.clearCache()
return this.http.post<ShareLinkBundleSummary>(
this.getResourceUrl(),
payload
)
}
rebuildBundle(bundleId: number): Observable<ShareLinkBundleSummary> {
this.clearCache()
return this.http.post<ShareLinkBundleSummary>(
this.getResourceUrl(bundleId, 'rebuild'),
{}
)
}
listAllBundles(): Observable<ShareLinkBundleSummary[]> {
return this.list(1, 1000, 'created', true).pipe(
map((response) => response.results)
)
}
}

View File

@ -12,8 +12,8 @@ from documents.models import Note
from documents.models import PaperlessTask from documents.models import PaperlessTask
from documents.models import SavedView from documents.models import SavedView
from documents.models import SavedViewFilterRule from documents.models import SavedViewFilterRule
from documents.models import ShareBundle
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.tasks import update_document_parent_tags from documents.tasks import update_document_parent_tags
@ -186,7 +186,7 @@ class ShareLinksAdmin(GuardedModelAdmin):
return super().get_queryset(request).select_related("document__correspondent") return super().get_queryset(request).select_related("document__correspondent")
class ShareBundleAdmin(GuardedModelAdmin): class ShareLinkBundleAdmin(GuardedModelAdmin):
list_display = ("created", "status", "expiration", "owner", "slug") list_display = ("created", "status", "expiration", "owner", "slug")
list_filter = ("status", "created", "expiration", "owner") list_filter = ("status", "created", "expiration", "owner")
search_fields = ("slug",) search_fields = ("slug",)
@ -233,7 +233,7 @@ admin.site.register(StoragePath, StoragePathAdmin)
admin.site.register(PaperlessTask, TaskAdmin) admin.site.register(PaperlessTask, TaskAdmin)
admin.site.register(Note, NotesAdmin) admin.site.register(Note, NotesAdmin)
admin.site.register(ShareLink, ShareLinksAdmin) admin.site.register(ShareLink, ShareLinksAdmin)
admin.site.register(ShareBundle, ShareBundleAdmin) admin.site.register(ShareLinkBundle, ShareLinkBundleAdmin)
admin.site.register(CustomField, CustomFieldsAdmin) admin.site.register(CustomField, CustomFieldsAdmin)
admin.site.register(CustomFieldInstance, CustomFieldInstancesAdmin) admin.site.register(CustomFieldInstance, CustomFieldInstancesAdmin)

View File

@ -38,8 +38,8 @@ from documents.models import CustomFieldInstance
from documents.models import Document from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import PaperlessTask from documents.models import PaperlessTask
from documents.models import ShareBundle
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
@ -794,11 +794,11 @@ class ShareLinkFilterSet(FilterSet):
} }
class ShareBundleFilterSet(FilterSet): class ShareLinkBundleFilterSet(FilterSet):
documents = Filter(method="filter_documents") documents = Filter(method="filter_documents")
class Meta: class Meta:
model = ShareBundle model = ShareLinkBundle
fields = { fields = {
"created": DATETIME_KWARGS, "created": DATETIME_KWARGS,
"expiration": DATETIME_KWARGS, "expiration": DATETIME_KWARGS,

View File

@ -11,7 +11,7 @@ from django.db import migrations
from django.db import models from django.db import models
def grant_sharebundle_permissions(apps, schema_editor): def grant_share_link_bundle_permissions(apps, schema_editor):
# Ensure newly introduced permissions are created for all apps # Ensure newly introduced permissions are created for all apps
for app_config in apps.get_app_configs(): for app_config in apps.get_app_configs():
app_config.models_module = True app_config.models_module = True
@ -22,27 +22,27 @@ def grant_sharebundle_permissions(apps, schema_editor):
if add_document_perm is None: if add_document_perm is None:
return return
sharebundle_permissions = Permission.objects.filter( share_bundle_permissions = Permission.objects.filter(
codename__contains="sharebundle", codename__contains="sharelinkbundle",
) )
users = User.objects.filter(user_permissions=add_document_perm).distinct() users = User.objects.filter(user_permissions=add_document_perm).distinct()
for user in users: for user in users:
user.user_permissions.add(*sharebundle_permissions) user.user_permissions.add(*share_bundle_permissions)
groups = Group.objects.filter(permissions=add_document_perm).distinct() groups = Group.objects.filter(permissions=add_document_perm).distinct()
for group in groups: for group in groups:
group.permissions.add(*sharebundle_permissions) group.permissions.add(*share_bundle_permissions)
def revoke_sharebundle_permissions(apps, schema_editor): def revoke_share_link_bundle_permissions(apps, schema_editor):
sharebundle_permissions = Permission.objects.filter( share_bundle_permissions = Permission.objects.filter(
codename__contains="sharebundle", codename__contains="sharelinkbundle",
) )
for user in User.objects.all(): for user in User.objects.all():
user.user_permissions.remove(*sharebundle_permissions) user.user_permissions.remove(*share_bundle_permissions)
for group in Group.objects.all(): for group in Group.objects.all():
group.permissions.remove(*sharebundle_permissions) group.permissions.remove(*share_bundle_permissions)
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -53,7 +53,7 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="ShareBundle", name="ShareLinkBundle",
fields=[ fields=[
( (
"id", "id",
@ -150,7 +150,7 @@ class Migration(migrations.Migration):
blank=True, blank=True,
null=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, on_delete=django.db.models.deletion.SET_NULL,
related_name="share_bundles", related_name="share_link_bundles",
to=settings.AUTH_USER_MODEL, to=settings.AUTH_USER_MODEL,
verbose_name="owner", verbose_name="owner",
), ),
@ -170,21 +170,21 @@ class Migration(migrations.Migration):
], ],
options={ options={
"ordering": ("-created",), "ordering": ("-created",),
"verbose_name": "share bundle", "verbose_name": "share link bundle",
"verbose_name_plural": "share bundles", "verbose_name_plural": "share link bundles",
}, },
), ),
migrations.AddField( migrations.AddField(
model_name="sharebundle", model_name="sharelinkbundle",
name="documents", name="documents",
field=models.ManyToManyField( field=models.ManyToManyField(
related_name="share_bundles", related_name="share_link_bundles",
to="documents.document", to="documents.document",
verbose_name="documents", verbose_name="documents",
), ),
), ),
migrations.RunPython( migrations.RunPython(
grant_sharebundle_permissions, grant_share_link_bundle_permissions,
reverse_code=revoke_sharebundle_permissions, reverse_code=revoke_share_link_bundle_permissions,
), ),
] ]

View File

@ -777,7 +777,7 @@ class ShareLink(SoftDeleteModel):
return f"Share Link for {self.document.title}" return f"Share Link for {self.document.title}"
class ShareBundle(SoftDeleteModel): class ShareLinkBundle(SoftDeleteModel):
class Status(models.TextChoices): class Status(models.TextChoices):
PENDING = ("pending", _("Pending")) PENDING = ("pending", _("Pending"))
PROCESSING = ("processing", _("Processing")) PROCESSING = ("processing", _("Processing"))
@ -786,8 +786,8 @@ class ShareBundle(SoftDeleteModel):
class Meta: class Meta:
ordering = ("-created",) ordering = ("-created",)
verbose_name = _("share bundle") verbose_name = _("share link bundle")
verbose_name_plural = _("share bundles") verbose_name_plural = _("share link bundles")
created = models.DateTimeField( created = models.DateTimeField(
_("created"), _("created"),
@ -816,7 +816,7 @@ class ShareBundle(SoftDeleteModel):
User, User,
blank=True, blank=True,
null=True, null=True,
related_name="share_bundles", related_name="share_link_bundles",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_("owner"), verbose_name=_("owner"),
) )
@ -858,12 +858,12 @@ class ShareBundle(SoftDeleteModel):
documents = models.ManyToManyField( documents = models.ManyToManyField(
"documents.Document", "documents.Document",
related_name="share_bundles", related_name="share_link_bundles",
verbose_name=_("documents"), verbose_name=_("documents"),
) )
def __str__(self): def __str__(self):
return _("Share bundle %(slug)s") % {"slug": self.slug} return _("Share link bundle %(slug)s") % {"slug": self.slug}
@property @property
def absolute_file_path(self) -> Path | None: def absolute_file_path(self) -> Path | None:

View File

@ -58,8 +58,8 @@ from documents.models import Note
from documents.models import PaperlessTask from documents.models import PaperlessTask
from documents.models import SavedView from documents.models import SavedView
from documents.models import SavedViewFilterRule from documents.models import SavedViewFilterRule
from documents.models import ShareBundle
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import UiSettings from documents.models import UiSettings
@ -2130,7 +2130,7 @@ class ShareLinkSerializer(OwnedObjectSerializer):
return super().create(validated_data) return super().create(validated_data)
class ShareBundleSerializer(OwnedObjectSerializer): class ShareLinkBundleSerializer(OwnedObjectSerializer):
document_ids = serializers.ListField( document_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1), child=serializers.IntegerField(min_value=1),
allow_empty=False, allow_empty=False,
@ -2149,7 +2149,7 @@ class ShareBundleSerializer(OwnedObjectSerializer):
document_count = SerializerMethodField() document_count = SerializerMethodField()
class Meta: class Meta:
model = ShareBundle model = ShareLinkBundle
fields = ( fields = (
"id", "id",
"created", "created",
@ -2198,7 +2198,7 @@ class ShareBundleSerializer(OwnedObjectSerializer):
else: else:
validated_data["expiration"] = None validated_data["expiration"] = None
share_bundle = super().create(validated_data) share_link_bundle = super().create(validated_data)
if documents is None: if documents is None:
documents = list( documents = list(
@ -2224,12 +2224,12 @@ class ShareBundleSerializer(OwnedObjectSerializer):
) )
ordered_documents = [documents_by_id[doc_id] for doc_id in document_ids] ordered_documents = [documents_by_id[doc_id] for doc_id in document_ids]
share_bundle.documents.set(ordered_documents) share_link_bundle.documents.set(ordered_documents)
share_bundle.document_total = len(ordered_documents) share_link_bundle.document_total = len(ordered_documents)
return share_bundle return share_link_bundle
def get_document_count(self, obj: ShareBundle) -> int: def get_document_count(self, obj: ShareLinkBundle) -> int:
count = getattr(obj, "document_total", None) count = getattr(obj, "document_total", None)
if count is not None: if count is not None:
return count return count

View File

@ -43,8 +43,8 @@ from documents.models import CustomFieldInstance
from documents.models import Document from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import PaperlessTask from documents.models import PaperlessTask
from documents.models import ShareBundle
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import Workflow from documents.models import Workflow
@ -572,17 +572,19 @@ def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None:
@shared_task @shared_task
def build_share_bundle(bundle_id: int): def build_share_link_bundle(bundle_id: int):
try: try:
bundle = ( bundle = (
ShareBundle.objects.filter(pk=bundle_id).prefetch_related("documents").get() ShareLinkBundle.objects.filter(pk=bundle_id)
.prefetch_related("documents")
.get()
) )
except ShareBundle.DoesNotExist: except ShareLinkBundle.DoesNotExist:
logger.warning("Share bundle %s no longer exists.", bundle_id) logger.warning("Share link bundle %s no longer exists.", bundle_id)
return return
bundle.remove_file() bundle.remove_file()
bundle.status = ShareBundle.Status.PROCESSING bundle.status = ShareLinkBundle.Status.PROCESSING
bundle.last_error = "" bundle.last_error = ""
bundle.size_bytes = None bundle.size_bytes = None
bundle.built_at = None bundle.built_at = None
@ -617,7 +619,7 @@ def build_share_bundle(bundle_id: int):
for document in documents: for document in documents:
strategy.add_document(document) strategy.add_document(document)
output_dir = settings.SHARE_BUNDLE_DIR output_dir = settings.SHARE_LINK_BUNDLE_DIR
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
final_path = (output_dir / f"{bundle.slug}.zip").resolve() final_path = (output_dir / f"{bundle.slug}.zip").resolve()
if final_path.exists(): if final_path.exists():
@ -629,7 +631,7 @@ def build_share_bundle(bundle_id: int):
except ValueError: except ValueError:
bundle.file_path = str(final_path) bundle.file_path = str(final_path)
bundle.size_bytes = final_path.stat().st_size bundle.size_bytes = final_path.stat().st_size
bundle.status = ShareBundle.Status.READY bundle.status = ShareLinkBundle.Status.READY
bundle.built_at = timezone.now() bundle.built_at = timezone.now()
bundle.last_error = "" bundle.last_error = ""
bundle.save( bundle.save(
@ -641,10 +643,14 @@ def build_share_bundle(bundle_id: int):
"last_error", "last_error",
], ],
) )
logger.info("Built share bundle %s", bundle.pk) logger.info("Built share link bundle %s", bundle.pk)
except Exception as exc: except Exception as exc:
logger.exception("Failed to build share bundle %s: %s", bundle_id, exc) logger.exception(
bundle.status = ShareBundle.Status.FAILED "Failed to build share link bundle %s: %s",
bundle_id,
exc,
)
bundle.status = ShareLinkBundle.Status.FAILED
bundle.last_error = str(exc) bundle.last_error = str(exc)
bundle.save(update_fields=["status", "last_error"]) bundle.save(update_fields=["status", "last_error"])
try: try:
@ -661,9 +667,9 @@ def build_share_bundle(bundle_id: int):
@shared_task @shared_task
def cleanup_expired_share_bundles(): def cleanup_expired_share_link_bundles():
now = timezone.now() now = timezone.now()
expired_qs = ShareBundle.objects.filter( expired_qs = ShareLinkBundle.objects.filter(
deleted_at__isnull=True, deleted_at__isnull=True,
expiration__isnull=False, expiration__isnull=False,
expiration__lt=now, expiration__lt=now,
@ -675,9 +681,9 @@ def cleanup_expired_share_bundles():
bundle.hard_delete() bundle.hard_delete()
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
"Failed to delete expired share bundle %s: %s", "Failed to delete expired share link bundle %s: %s",
bundle.pk, bundle.pk,
exc, exc,
) )
if count: if count:
logger.info("Deleted %s expired share bundle(s)", count) logger.info("Deleted %s expired share link bundle(s)", count)

View File

@ -119,7 +119,7 @@ from documents.filters import DocumentTypeFilterSet
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
from documents.filters import ObjectOwnedPermissionsFilter from documents.filters import ObjectOwnedPermissionsFilter
from documents.filters import PaperlessTaskFilterSet from documents.filters import PaperlessTaskFilterSet
from documents.filters import ShareBundleFilterSet from documents.filters import ShareLinkBundleFilterSet
from documents.filters import ShareLinkFilterSet from documents.filters import ShareLinkFilterSet
from documents.filters import StoragePathFilterSet from documents.filters import StoragePathFilterSet
from documents.filters import TagFilterSet from documents.filters import TagFilterSet
@ -135,8 +135,8 @@ from documents.models import DocumentType
from documents.models import Note from documents.models import Note
from documents.models import PaperlessTask from documents.models import PaperlessTask
from documents.models import SavedView from documents.models import SavedView
from documents.models import ShareBundle
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import UiSettings from documents.models import UiSettings
@ -170,7 +170,7 @@ from documents.serialisers import PostDocumentSerializer
from documents.serialisers import RunTaskViewSerializer from documents.serialisers import RunTaskViewSerializer
from documents.serialisers import SavedViewSerializer from documents.serialisers import SavedViewSerializer
from documents.serialisers import SearchResultSerializer from documents.serialisers import SearchResultSerializer
from documents.serialisers import ShareBundleSerializer from documents.serialisers import ShareLinkBundleSerializer
from documents.serialisers import ShareLinkSerializer from documents.serialisers import ShareLinkSerializer
from documents.serialisers import StoragePathSerializer from documents.serialisers import StoragePathSerializer
from documents.serialisers import StoragePathTestSerializer from documents.serialisers import StoragePathTestSerializer
@ -183,7 +183,7 @@ from documents.serialisers import WorkflowActionSerializer
from documents.serialisers import WorkflowSerializer from documents.serialisers import WorkflowSerializer
from documents.serialisers import WorkflowTriggerSerializer from documents.serialisers import WorkflowTriggerSerializer
from documents.signals import document_updated from documents.signals import document_updated
from documents.tasks import build_share_bundle from documents.tasks import build_share_link_bundle
from documents.tasks import consume_file from documents.tasks import consume_file
from documents.tasks import empty_trash from documents.tasks import empty_trash
from documents.tasks import index_optimize from documents.tasks import index_optimize
@ -2604,12 +2604,12 @@ class ShareLinkViewSet(ModelViewSet, PassUserMixin):
ordering_fields = ("created", "expiration", "document") ordering_fields = ("created", "expiration", "document")
class ShareBundleViewSet(ModelViewSet, PassUserMixin): class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin):
model = ShareBundle model = ShareLinkBundle
queryset = ShareBundle.objects.all() queryset = ShareLinkBundle.objects.all()
serializer_class = ShareBundleSerializer serializer_class = ShareLinkBundleSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = ( filter_backends = (
@ -2617,7 +2617,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
OrderingFilter, OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter, ObjectOwnedOrGrantedPermissionsFilter,
) )
filterset_class = ShareBundleFilterSet filterset_class = ShareLinkBundleFilterSet
ordering_fields = ("created", "expiration", "status") ordering_fields = ("created", "expiration", "status")
def get_queryset(self): def get_queryset(self):
@ -2667,7 +2667,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
documents=ordered_documents, documents=ordered_documents,
) )
bundle.remove_file() bundle.remove_file()
bundle.status = ShareBundle.Status.PENDING bundle.status = ShareLinkBundle.Status.PENDING
bundle.last_error = "" bundle.last_error = ""
bundle.size_bytes = None bundle.size_bytes = None
bundle.built_at = None bundle.built_at = None
@ -2681,7 +2681,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
"file_path", "file_path",
], ],
) )
build_share_bundle.delay(bundle.pk) build_share_link_bundle.delay(bundle.pk)
bundle.document_total = len(ordered_documents) bundle.document_total = len(ordered_documents)
response_serializer = self.get_serializer(bundle) response_serializer = self.get_serializer(bundle)
headers = self.get_success_headers(response_serializer.data) headers = self.get_success_headers(response_serializer.data)
@ -2694,13 +2694,13 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
def rebuild(self, request, pk=None): def rebuild(self, request, pk=None):
bundle = self.get_object() bundle = self.get_object()
if bundle.status == ShareBundle.Status.PROCESSING: if bundle.status == ShareLinkBundle.Status.PROCESSING:
return Response( return Response(
{"detail": _("Bundle is already being processed.")}, {"detail": _("Bundle is already being processed.")},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
bundle.remove_file() bundle.remove_file()
bundle.status = ShareBundle.Status.PENDING bundle.status = ShareLinkBundle.Status.PENDING
bundle.last_error = "" bundle.last_error = ""
bundle.size_bytes = None bundle.size_bytes = None
bundle.built_at = None bundle.built_at = None
@ -2714,7 +2714,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
"file_path", "file_path",
], ],
) )
build_share_bundle.delay(bundle.pk) build_share_link_bundle.delay(bundle.pk)
bundle.document_total = ( bundle.document_total = (
getattr(bundle, "document_total", None) or bundle.documents.count() getattr(bundle, "document_total", None) or bundle.documents.count()
) )
@ -2740,35 +2740,32 @@ class SharedLinkView(View):
disposition="inline", disposition="inline",
) )
share_bundle = ShareBundle.objects.filter(slug=slug).first() bundle = ShareLinkBundle.objects.filter(slug=slug).first()
if share_bundle is None: if bundle is None:
return HttpResponseRedirect("/accounts/login/?sharelink_notfound=1") return HttpResponseRedirect("/accounts/login/?sharelink_notfound=1")
if ( if bundle.expiration is not None and bundle.expiration < timezone.now():
share_bundle.expiration is not None
and share_bundle.expiration < timezone.now()
):
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1") return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
if share_bundle.status in { if bundle.status in {
ShareBundle.Status.PENDING, ShareLinkBundle.Status.PENDING,
ShareBundle.Status.PROCESSING, ShareLinkBundle.Status.PROCESSING,
}: }:
return HttpResponse( return HttpResponse(
_( _(
"The shared bundle is still being prepared. Please try again later.", "The share link bundle is still being prepared. Please try again later.",
), ),
status=status.HTTP_202_ACCEPTED, status=status.HTTP_202_ACCEPTED,
) )
if share_bundle.status == ShareBundle.Status.FAILED: if bundle.status == ShareLinkBundle.Status.FAILED:
share_bundle.remove_file() bundle.remove_file()
share_bundle.status = ShareBundle.Status.PENDING bundle.status = ShareLinkBundle.Status.PENDING
share_bundle.last_error = "" bundle.last_error = ""
share_bundle.size_bytes = None bundle.size_bytes = None
share_bundle.built_at = None bundle.built_at = None
share_bundle.file_path = "" bundle.file_path = ""
share_bundle.save( bundle.save(
update_fields=[ update_fields=[
"status", "status",
"last_error", "last_error",
@ -2777,22 +2774,22 @@ class SharedLinkView(View):
"file_path", "file_path",
], ],
) )
build_share_bundle.delay(share_bundle.pk) build_share_link_bundle.delay(bundle.pk)
return HttpResponse( return HttpResponse(
_( _(
"The shared bundle is temporarily unavailable. A rebuild has been scheduled. Please try again later.", "The share link bundle is temporarily unavailable. A rebuild has been scheduled. Please try again later.",
), ),
status=status.HTTP_503_SERVICE_UNAVAILABLE, status=status.HTTP_503_SERVICE_UNAVAILABLE,
) )
file_path = share_bundle.absolute_file_path file_path = bundle.absolute_file_path
if file_path is None or not file_path.exists(): if file_path is None or not file_path.exists():
share_bundle.status = ShareBundle.Status.PENDING bundle.status = ShareLinkBundle.Status.PENDING
share_bundle.last_error = "" bundle.last_error = ""
share_bundle.size_bytes = None bundle.size_bytes = None
share_bundle.built_at = None bundle.built_at = None
share_bundle.file_path = "" bundle.file_path = ""
share_bundle.save( bundle.save(
update_fields=[ update_fields=[
"status", "status",
"last_error", "last_error",
@ -2801,16 +2798,16 @@ class SharedLinkView(View):
"file_path", "file_path",
], ],
) )
build_share_bundle.delay(share_bundle.pk) build_share_link_bundle.delay(bundle.pk)
return HttpResponse( return HttpResponse(
_( _(
"The shared bundle is being prepared. Please try again later.", "The share link bundle is being prepared. Please try again later.",
), ),
status=status.HTTP_202_ACCEPTED, status=status.HTTP_202_ACCEPTED,
) )
response = FileResponse(file_path.open("rb"), content_type="application/zip") response = FileResponse(file_path.open("rb"), content_type="application/zip")
short_slug = share_bundle.slug[:12] short_slug = bundle.slug[:12]
download_name = f"paperless-share-{short_slug}.zip" download_name = f"paperless-share-{short_slug}.zip"
filename_normalized = ( filename_normalized = (
normalize("NFKD", download_name) normalize("NFKD", download_name)

View File

@ -231,11 +231,11 @@ def _parse_beat_schedule() -> dict:
}, },
}, },
{ {
"name": "Cleanup expired share bundles", "name": "Cleanup expired share link bundles",
"env_key": "PAPERLESS_SHARE_BUNDLE_CLEANUP_CRON", "env_key": "PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON",
# Default daily at 02:00 # Default daily at 02:00
"env_default": "0 2 * * *", "env_default": "0 2 * * *",
"task": "documents.tasks.cleanup_expired_share_bundles", "task": "documents.tasks.cleanup_expired_share_link_bundles",
"options": { "options": {
# 1 hour before default schedule sends again # 1 hour before default schedule sends again
"expires": 23.0 * 60.0 * 60.0, "expires": 23.0 * 60.0 * 60.0,
@ -279,7 +279,7 @@ MEDIA_ROOT = __get_path("PAPERLESS_MEDIA_ROOT", BASE_DIR.parent / "media")
ORIGINALS_DIR = MEDIA_ROOT / "documents" / "originals" ORIGINALS_DIR = MEDIA_ROOT / "documents" / "originals"
ARCHIVE_DIR = MEDIA_ROOT / "documents" / "archive" ARCHIVE_DIR = MEDIA_ROOT / "documents" / "archive"
THUMBNAIL_DIR = MEDIA_ROOT / "documents" / "thumbnails" THUMBNAIL_DIR = MEDIA_ROOT / "documents" / "thumbnails"
SHARE_BUNDLE_DIR = MEDIA_ROOT / "documents" / "share_bundles" SHARE_LINK_BUNDLE_DIR = MEDIA_ROOT / "documents" / "share_link_bundles"
DATA_DIR = __get_path("PAPERLESS_DATA_DIR", BASE_DIR.parent / "data") DATA_DIR = __get_path("PAPERLESS_DATA_DIR", BASE_DIR.parent / "data")

View File

@ -29,8 +29,8 @@ from documents.views import RemoteVersionView
from documents.views import SavedViewViewSet from documents.views import SavedViewViewSet
from documents.views import SearchAutoCompleteView from documents.views import SearchAutoCompleteView
from documents.views import SelectionDataView from documents.views import SelectionDataView
from documents.views import ShareBundleViewSet
from documents.views import SharedLinkView from documents.views import SharedLinkView
from documents.views import ShareLinkBundleViewSet
from documents.views import ShareLinkViewSet from documents.views import ShareLinkViewSet
from documents.views import StatisticsView from documents.views import StatisticsView
from documents.views import StoragePathViewSet from documents.views import StoragePathViewSet
@ -73,7 +73,7 @@ api_router.register(r"users", UserViewSet, basename="users")
api_router.register(r"groups", GroupViewSet, basename="groups") api_router.register(r"groups", GroupViewSet, basename="groups")
api_router.register(r"mail_accounts", MailAccountViewSet) api_router.register(r"mail_accounts", MailAccountViewSet)
api_router.register(r"mail_rules", MailRuleViewSet) api_router.register(r"mail_rules", MailRuleViewSet)
api_router.register(r"share_bundles", ShareBundleViewSet) api_router.register(r"share_link_bundles", ShareLinkBundleViewSet)
api_router.register(r"share_links", ShareLinkViewSet) api_router.register(r"share_links", ShareLinkViewSet)
api_router.register(r"workflow_triggers", WorkflowTriggerViewSet) api_router.register(r"workflow_triggers", WorkflowTriggerViewSet)
api_router.register(r"workflow_actions", WorkflowActionViewSet) api_router.register(r"workflow_actions", WorkflowActionViewSet)