Skip to content

Commit 8ac40cd

Browse files
committed
feat(files): add overwrite confirmation dialog for file uploads and adapt API to support overwrite functionality
1 parent 7fc8311 commit 8ac40cd

File tree

22 files changed

+157
-70
lines changed

22 files changed

+157
-70
lines changed

backend/src/applications/files/files.controller.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe(FilesController.name, () => {
104104
})
105105

106106
it('upload() should call filesMethods.upload(req, res)', async () => {
107-
await filesController.upload(fakeReq, fakeRes)
107+
await filesController.uploadCreate(fakeReq, fakeRes)
108108

109109
expect(filesMethodsMock.upload).toHaveBeenCalledWith(fakeReq, fakeRes)
110110
})

backend/src/applications/files/files.controller.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Move,
1616
ParseIntPipe,
1717
Post,
18+
Put,
1819
Query,
1920
Req,
2021
Res,
@@ -73,7 +74,12 @@ export class FilesController {
7374
}
7475

7576
@Post(`${FILES_ROUTE.OPERATION}/${FILE_OPERATION.UPLOAD}/*`)
76-
async upload(@Req() req: FastifySpaceRequest, @Res({ passthrough: true }) res: FastifyReply): Promise<void> {
77+
async uploadCreate(@Req() req: FastifySpaceRequest, @Res({ passthrough: true }) res: FastifyReply): Promise<void> {
78+
return this.filesMethods.upload(req, res)
79+
}
80+
81+
@Put(`${FILES_ROUTE.OPERATION}/${FILE_OPERATION.UPLOAD}/*`)
82+
async uploadOverwrite(@Req() req: FastifySpaceRequest, @Res({ passthrough: true }) res: FastifyReply): Promise<void> {
7783
return this.filesMethods.upload(req, res)
7884
}
7985

backend/src/applications/files/services/files-manager.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export class FilesManager {
178178
async saveMultipart(user: UserModel, space: SpaceEnv, req: FastifySpaceRequest) {
179179
/* Accepted methods:
180180
POST: create new resource
181-
PUT: create or update new resource (even if parent path does not exist)
181+
PUT: create or update new resource (even if a parent path does not exist)
182182
*/
183183
const realParentPath = dirName(space.realPath)
184184

backend/src/applications/files/services/files-methods.service.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ export class FilesMethods {
4343
try {
4444
await this.filesManager.saveMultipart(req.user, req.space, req)
4545
} catch (e) {
46-
// if error we need to close the stream
47-
// req.raw.destroy()
4846
this.logger.error(`${this.upload.name} - unable to ${FILE_OPERATION.UPLOAD} ${req.space.url} : ${e}`)
4947
return res
5048
.header('Connection', 'close')
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!--
2+
~ Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com>
3+
~ This file is part of Sync-in | The open source file sync and share solution
4+
~ See the LICENSE file for licensing details
5+
-->
6+
7+
<div class="modal-header">
8+
<h4 class="modal-title" l10nTranslate>
9+
<fa-icon [icon]="icons.faFileShield" class="me-1"></fa-icon>
10+
<span l10nTranslate>Overwrite Existing File(s)</span>
11+
</h4>
12+
<button (click)="layout.closeDialog()" aria-label="Close" class="btn-close btn-close-white" type="button"></button>
13+
</div>
14+
<div class="modal-body">
15+
<div l10nTranslate>Do you want to replace the existing file(s)?</div>
16+
<ul class="my-2" style="padding-left: inherit">
17+
@for (file of files; track $index) {
18+
<li><b>{{ file.name }}</b></li>
19+
}
20+
</ul>
21+
</div>
22+
<div class="modal-footer">
23+
<button (click)="onAction(false)" class="btn btn-sm btn-secondary" data-dismiss="modal" type="button" l10nTranslate>Cancel</button>
24+
<button (click)="onAction(true)" [disabled]="submitted" class="btn btn-sm btn-danger" type="button" l10nTranslate>Confirm</button>
25+
</div>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com>
3+
* This file is part of Sync-in | The open source file sync and share solution
4+
* See the LICENSE file for licensing details
5+
*/
6+
7+
import { Component, HostListener, inject, Input, output } from '@angular/core'
8+
import { FaIconComponent } from '@fortawesome/angular-fontawesome'
9+
import { faFileShield } from '@fortawesome/free-solid-svg-icons'
10+
import { L10nTranslateDirective } from 'angular-l10n'
11+
import { LayoutService } from '../../../../layout/layout.service'
12+
13+
@Component({
14+
selector: 'app-files-overwrite-dialog',
15+
imports: [FaIconComponent, L10nTranslateDirective],
16+
templateUrl: 'files-overwrite-dialog.component.html'
17+
})
18+
export class FilesOverwriteDialogComponent {
19+
@Input() files: File[] = []
20+
public overwrite = output<boolean>()
21+
protected layout = inject(LayoutService)
22+
protected readonly icons = { faFileShield }
23+
protected submitted = false
24+
25+
@HostListener('document:keyup.enter')
26+
onEnter() {
27+
this.onAction(true)
28+
}
29+
30+
@HostListener('document:keyup.escape')
31+
onEsc() {
32+
this.onAction(false)
33+
}
34+
35+
onAction(overwrite: boolean) {
36+
this.submitted = overwrite
37+
this.overwrite.emit(overwrite)
38+
this.layout.closeDialog()
39+
}
40+
}

frontend/src/app/applications/files/services/files-upload.service.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class FilesUploadService {
2222
private readonly filesService = inject(FilesService)
2323
private readonly filesTasksService = inject(FilesTasksService)
2424

25-
async addFiles(files: FileUpload[]) {
25+
async addFiles(files: FileUpload[], overwrite: boolean) {
2626
const apiRoute = `${API_FILES_OPERATION_UPLOAD}/${this.filesService.currentRoute}`
2727
const taskReqs: [FileTask, Observable<any>][] = []
2828

@@ -32,7 +32,7 @@ export class FilesUploadService {
3232
const task: FileTask = this.filesTasksService.createUploadTask(path, name, data.size)
3333
taskReqs.unshift([
3434
task,
35-
this.uploadFiles(`${apiRoute}/${key}`, data.form).pipe(
35+
this.uploadFiles(`${apiRoute}/${key}`, data.form, overwrite).pipe(
3636
filter((ev: any) => ev.type === HttpEventType.UploadProgress),
3737
tap((ev: HttpUploadProgressEvent) => this.updateProgress(task, ev))
3838
)
@@ -56,16 +56,20 @@ export class FilesUploadService {
5656
}
5757
}
5858

59-
onDropFiles(ev: any) {
59+
onDropFiles(ev: any, overwrite: boolean) {
6060
if (ev.dataTransfer.items && ev.dataTransfer.items[0]?.webkitGetAsEntry) {
61-
this.webkitReadDataTransfer(ev)
61+
this.webkitReadDataTransfer(ev, overwrite)
6262
} else {
63-
this.addFiles(ev.dataTransfer.files).catch(console.error)
63+
this.addFiles(ev.dataTransfer.files, overwrite).catch(console.error)
6464
}
6565
}
6666

67-
private uploadFiles(url: string, form: FormData) {
68-
return this.http.post(url, form, { reportProgress: true, observe: 'events' })
67+
private uploadFiles(url: string, form: FormData, overwrite: boolean) {
68+
return this.http.request(overwrite ? 'put' : 'post', url, {
69+
body: form,
70+
reportProgress: true,
71+
observe: 'events'
72+
})
6973
}
7074

7175
private updateProgress(task: FileTask, ev: HttpUploadProgressEvent) {
@@ -87,7 +91,7 @@ export class FilesUploadService {
8791
return sort
8892
}
8993

90-
private webkitReadDataTransfer(ev: any) {
94+
private webkitReadDataTransfer(ev: any, overwrite: boolean) {
9195
let queue = ev.dataTransfer.items.length
9296
const files: FileUpload[] = []
9397
const readDirectory = (reader: any) => {
@@ -120,7 +124,7 @@ export class FilesUploadService {
120124
}
121125
const decrement = () => {
122126
if (--queue == 0) {
123-
this.addFiles(files).catch(console.error)
127+
this.addFiles(files, overwrite).catch(console.error)
124128
}
125129
}
126130

frontend/src/app/applications/files/services/files.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ import type { FileContent } from '@sync-in-server/backend/src/applications/files
3232
import type { FileRecent } from '@sync-in-server/backend/src/applications/files/schemas/file-recent.interface'
3333
import { API_SPACES_TREE } from '@sync-in-server/backend/src/applications/spaces/constants/routes'
3434
import { forbiddenChars, isValidFileName } from '@sync-in-server/backend/src/common/shared'
35+
import { BsModalRef } from 'ngx-bootstrap/modal'
3536
import { firstValueFrom, map, Observable, Subject } from 'rxjs'
3637
import { downloadWithAnchor } from '../../../common/utils/functions'
3738
import { TAB_MENU } from '../../../layout/layout.interfaces'
3839
import { LayoutService } from '../../../layout/layout.service'
3940
import { StoreService } from '../../../store/store.service'
41+
import { FilesOverwriteDialogComponent } from '../components/dialogs/files-overwrite-dialog.component'
4042
import { FilesViewerDialogComponent } from '../components/dialogs/files-viewer-dialog.component'
4143
import { FileContentModel } from '../models/file-content.model'
4244
import { FileRecentModel } from '../models/file-recent.model'
@@ -210,6 +212,17 @@ export class FilesService {
210212
)
211213
}
212214

215+
async openOverwriteDialog(files: File[]): Promise<boolean> {
216+
const overwriteDialog: BsModalRef<FilesOverwriteDialogComponent> = this.layout.openDialog(FilesOverwriteDialogComponent, null, {
217+
initialState: {
218+
files: files
219+
} as FilesOverwriteDialogComponent
220+
})
221+
return new Promise<boolean>((resolve) => {
222+
overwriteDialog.content.overwrite.subscribe(resolve)
223+
})
224+
}
225+
213226
async openViewerDialog(mode: 'view' | 'edit', currentFile: FileModel) {
214227
this.http.head(currentFile.dataUrl).subscribe({
215228
next: async () => {

frontend/src/app/applications/spaces/components/spaces-browser.component.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -556,32 +556,33 @@ export class SpacesBrowserComponent implements OnInit, AfterViewInit, OnDestroy
556556
}
557557
}
558558

559-
onUploadFiles(ev: { files: File[] }, isDirectory = false) {
559+
async onUploadFiles(ev: { files: File[] }, isDirectory = false) {
560+
let doOverwrite = false
560561
if (isDirectory) {
561562
const dirName = ev.files[0].webkitRelativePath.split('/')[0]
562-
if (this.files.find((f) => f.name === dirName)) {
563-
this.layout.sendNotification('warning', 'This folder already exists', dirName)
564-
return
563+
const dirExist = this.files.find((f) => f.name.toLowerCase() === dirName.toLowerCase())
564+
if (dirExist) {
565+
doOverwrite = await this.filesService.openOverwriteDialog([{ name: dirName } as File])
566+
if (!doOverwrite) return
565567
}
566568
} else {
567-
for (const uF of ev.files) {
568-
if (this.files.find((f) => uF.name === f.name)) {
569-
this.layout.sendNotification('warning', 'This file already exists', uF.name)
570-
return
571-
}
569+
const exist: File[] = [...ev.files].filter((f: File) => this.files.some((x) => x.name.toLowerCase() === f.name.toLowerCase()))
570+
if (exist.length > 0) {
571+
doOverwrite = await this.filesService.openOverwriteDialog(exist)
572+
if (!doOverwrite) return
572573
}
573574
}
574-
this.filesUpload.addFiles(ev.files).catch(console.error)
575+
this.filesUpload.addFiles(ev.files, doOverwrite).catch(console.error)
575576
}
576577

577-
onDropFiles(ev: { dataTransfer: { files: File[] } }) {
578-
for (const uF of ev.dataTransfer.files) {
579-
if (this.files.find((f) => uF.name === f.name)) {
580-
this.layout.sendNotification('warning', 'This file already exists', uF.name)
581-
return
582-
}
578+
async onDropFiles(ev: { dataTransfer: { files: File[] } }) {
579+
let doOverwrite = false
580+
const exist: File[] = [...ev.dataTransfer.files].filter((f: File) => this.files.some((x) => x.name.toLowerCase() === f.name.toLowerCase()))
581+
if (exist.length > 0) {
582+
doOverwrite = await this.filesService.openOverwriteDialog(exist)
583+
if (!doOverwrite) return
583584
}
584-
this.filesUpload.onDropFiles(ev)
585+
this.filesUpload.onDropFiles(ev, doOverwrite)
585586
}
586587

587588
decompressFile() {

frontend/src/i18n/de.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,6 @@
536536
"Drop folder here": "Ordner hier ablegen",
537537
"This directory is already synced": "Dieses Verzeichnis wird bereits synchronisiert",
538538
"The parent directory is already synced": "Das übergeordnete Verzeichnis wird bereits synchronisiert",
539-
"This folder already exists": "Dieser Ordner existiert bereits",
540-
"This file already exists": "Diese Datei existiert bereits",
541539
"This directory is not accessible": "Dieses Verzeichnis ist nicht zugänglich",
542540
"This directory is read-only, you will not be able to modify it": "Dieses Verzeichnis ist schreibgeschützt; Sie können es nicht ändern",
543541
"Please select the server directory to sync, if it doesn't exist you can create it.": "Bitte wählen Sie das Serververzeichnis zur Synchronisierung aus; falls es nicht existiert, können Sie es erstellen.",
@@ -597,5 +595,7 @@
597595
"Valid with your TOTP code": "Mit Ihrem TOTP‑Code bestätigen",
598596
"The secret has expired": "Der geheime Schlüssel ist abgelaufen",
599597
"Keep these codes in a safe place. They will allow you to access your account if you lose access to your two-factor authentication.": "Bewahren Sie diese Codes an einem sicheren Ort auf. Sie ermöglichen Ihnen den Zugriff auf Ihr Konto, falls Sie den Zugriff auf Ihre Zwei‑Faktor‑Authentifizierung verlieren.",
600-
"These recovery codes are for one-time use only and will only be displayed here once.": "Diese Wiederherstellungscodes sind nur einmal verwendbar und werden hier nur einmal angezeigt."
598+
"These recovery codes are for one-time use only and will only be displayed here once.": "Diese Wiederherstellungscodes sind nur einmal verwendbar und werden hier nur einmal angezeigt.",
599+
"Overwrite Existing File(s)": "Vorhandene Datei(en) überschreiben",
600+
"Do you want to replace the existing file(s)?": "Möchten Sie die vorhandene(n) Datei(en) ersetzen?"
601601
}

0 commit comments

Comments
 (0)