This is an automated email from the ASF dual-hosted git repository.
linxinyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/master by this push:
new f4727a6209 feat: enable configurable multipart upload settings using
database (#3622)
f4727a6209 is described below
commit f4727a6209bfb132debe2ee6a02b10fdb2935f6a
Author: Xuan Gu <[email protected]>
AuthorDate: Fri Aug 8 19:48:59 2025 -0700
feat: enable configurable multipart upload settings using database (#3622)
### **Purpose**
This PR extends the [configuration-CRUD
framework](https://github.com/Texera/texera/issues/3528) in Admin
Settings to include multipart-upload parameters. Admins can adjust
global defaults for max file upload size, chunk size, and concurrent
upload count directly from Admin → General Settings → Dataset. Changes
take effect immediately without service restarts and persist in the
_site_settings_ table.
### **Changes**
- admin-settings.component.ts/html/scss: Added a Dataset card with three
controls (max file size, chunk size, concurrency).
- files-uploader.component.ts: Switched to reading the
“single-file-upload-maximum-size-mb” setting from the database.
- dataset-detail.component.ts/dataset.service.ts: Extended
multipartUpload() signature to accept partSizeMB and concurrencyLimit
and forward them to the backend.
- default.conf: Defined upload default values.
-
gui.conf/gui-config.service.ts/gui-config.service.mock.ts/GuiConfig.scala/ConfigResource.scala:
Migrated upload-setting storage to the new keys and removed the original
byte-based fields.
- app.module.ts: Imported NzInputNumberModule to enable number-input
controls in the admin UI.
### **Demonstration**
Settings Interface:
<img width="788" height="361" alt="Uploading"
src="https://github.com/user-attachments/assets/81fbaff7-dad0-4132-9454-2b07854dc280"
/>
---------
Co-authored-by: Xinyuan Lin <[email protected]>
---
.../texera/service/resource/ConfigResource.scala | 3 --
core/config/src/main/resources/default.conf | 12 +++++
core/config/src/main/resources/gui.conf | 15 ------
.../edu/uci/ics/texera/config/GuiConfig.scala | 8 ----
core/gui/src/app/app.module.ts | 2 +
.../app/common/service/gui-config.service.mock.ts | 3 --
core/gui/src/app/common/type/gui-config.ts | 3 --
.../admin/settings/admin-settings.component.html | 49 +++++++++++++++++++
.../admin/settings/admin-settings.component.scss | 14 ++++++
.../admin/settings/admin-settings.component.ts | 55 ++++++++++++++++++++++
.../files-uploader/files-uploader.component.ts | 20 +++++---
.../dataset-detail.component.ts | 30 ++++++++++--
.../service/user/dataset/dataset.service.ts | 10 ++--
13 files changed, 179 insertions(+), 45 deletions(-)
diff --git
a/core/config-service/src/main/scala/edu/uci/ics/texera/service/resource/ConfigResource.scala
b/core/config-service/src/main/scala/edu/uci/ics/texera/service/resource/ConfigResource.scala
index df1feaf7fb..23880f3c08 100644
---
a/core/config-service/src/main/scala/edu/uci/ics/texera/service/resource/ConfigResource.scala
+++
b/core/config-service/src/main/scala/edu/uci/ics/texera/service/resource/ConfigResource.scala
@@ -45,9 +45,6 @@ class ConfigResource {
"asyncRenderingEnabled" ->
GuiConfig.guiWorkflowWorkspaceAsyncRenderingEnabled,
"timetravelEnabled" -> GuiConfig.guiWorkflowWorkspaceTimetravelEnabled,
"productionSharedEditingServer" ->
GuiConfig.guiWorkflowWorkspaceProductionSharedEditingServer,
- "singleFileUploadMaximumSizeMB" ->
GuiConfig.guiDatasetSingleFileUploadMaximumSizeMB,
- "maxNumberOfConcurrentUploadingFileChunks" ->
GuiConfig.guiDatasetMaxNumberOfConcurrentUploadingFileChunks,
- "multipartUploadChunkSizeByte" ->
GuiConfig.guiDatasetMultipartUploadChunkSizeByte,
"defaultDataTransferBatchSize" ->
GuiConfig.guiWorkflowWorkspaceDefaultDataTransferBatchSize,
"workflowEmailNotificationEnabled" ->
GuiConfig.guiWorkflowWorkspaceWorkflowEmailNotificationEnabled,
"sharingComputingUnitEnabled" ->
ComputingUnitConfig.sharingComputingUnitEnabled,
diff --git a/core/config/src/main/resources/default.conf
b/core/config/src/main/resources/default.conf
index fe60ed0f35..fa6e949c1d 100644
--- a/core/config/src/main/resources/default.conf
+++ b/core/config/src/main/resources/default.conf
@@ -50,3 +50,15 @@ gui {
about_enabled = true
}
}
+
+dataset {
+ # the file size limit for dataset upload
+ single_file_upload_max_size_mb = 20
+
+ # the maximum number of file chunks that can be held in the memory;
+ # you may increase this number if your deployment environment has enough
memory resource
+ max_number_of_concurrent_uploading_file_chunks = 10
+
+ # the size of each chunk during the multipart upload of file
+ multipart_upload_chunk_size_mb = 50
+}
diff --git a/core/config/src/main/resources/gui.conf
b/core/config/src/main/resources/gui.conf
index ebdc4f2807..14d3c69403 100644
--- a/core/config/src/main/resources/gui.conf
+++ b/core/config/src/main/resources/gui.conf
@@ -101,19 +101,4 @@ gui {
workflow-email-notification-enabled = false
workflow-email-notification-enabled =
${?GUI_WORKFLOW_WORKSPACE_WORKFLOW_EMAIL_NOTIFICATION_ENABLED}
}
-
- dataset {
- # the file size limit for dataset upload
- single-file-upload-maximum-size-mb = 20
- single-file-upload-maximum-size-mb =
${?GUI_DATASET_SINGLE_FILE_UPLOAD_MAXIMUM_SIZE_MB}
-
- # the maximum number of file chunks that can be held in the memory;
- # you may increase this number if your deployment environment has enough
memory resource.
- max-number-of-concurrent-uploading-file-chunks = 10
- max-number-of-concurrent-uploading-file-chunks =
${?GUI_DATASET_MAX_NUMBER_OF_CONCURRENT_UPLOADING_FILE_CHUNKS}
-
- # the size of each chunk during the multipart upload of file
- multipart-upload-chunk-size-byte = 52428800 # 50 MB
- multipart-upload-chunk-size-byte =
${?GUI_DATASET_MULTIPART_UPLOAD_CHUNK_SIZE_BYTE}
- }
}
\ No newline at end of file
diff --git
a/core/config/src/main/scala/edu/uci/ics/texera/config/GuiConfig.scala
b/core/config/src/main/scala/edu/uci/ics/texera/config/GuiConfig.scala
index 4ffe495747..b64edcfe41 100644
--- a/core/config/src/main/scala/edu/uci/ics/texera/config/GuiConfig.scala
+++ b/core/config/src/main/scala/edu/uci/ics/texera/config/GuiConfig.scala
@@ -65,12 +65,4 @@ object GuiConfig {
conf.getInt("gui.workflow-workspace.operator-console-message-buffer-size")
val guiWorkflowWorkspaceWorkflowEmailNotificationEnabled: Boolean =
conf.getBoolean("gui.workflow-workspace.workflow-email-notification-enabled")
-
- // GUI Dataset Configuration
- val guiDatasetSingleFileUploadMaximumSizeMB: Int =
- conf.getInt("gui.dataset.single-file-upload-maximum-size-mb")
- val guiDatasetMaxNumberOfConcurrentUploadingFileChunks: Int =
- conf.getInt("gui.dataset.max-number-of-concurrent-uploading-file-chunks")
- val guiDatasetMultipartUploadChunkSizeByte: Long =
- conf.getLong("gui.dataset.multipart-upload-chunk-size-byte")
}
diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts
index f7f228a62e..13759e6e55 100644
--- a/core/gui/src/app/app.module.ts
+++ b/core/gui/src/app/app.module.ts
@@ -171,6 +171,7 @@ import { NzSliderModule } from "ng-zorro-antd/slider";
import { AdminSettingsComponent } from
"./dashboard/component/admin/settings/admin-settings.component";
import { catchError, of } from "rxjs";
import { FormlyRepeatDndComponent } from
"./common/formly/repeat-dnd/repeat-dnd.component";
+import { NzInputNumberModule } from "ng-zorro-antd/input-number";
registerLocaleData(en);
@@ -328,6 +329,7 @@ registerLocaleData(en);
NzEmptyModule,
NzDividerModule,
NzProgressModule,
+ NzInputNumberModule,
],
providers: [
provideNzI18n(en_US),
diff --git a/core/gui/src/app/common/service/gui-config.service.mock.ts
b/core/gui/src/app/common/service/gui-config.service.mock.ts
index 69b3b61bbb..60f7e7413c 100644
--- a/core/gui/src/app/common/service/gui-config.service.mock.ts
+++ b/core/gui/src/app/common/service/gui-config.service.mock.ts
@@ -42,9 +42,6 @@ export class MockGuiConfigService {
timetravelEnabled: false,
productionSharedEditingServer: false,
pythonLanguageServerPort: "3000",
- singleFileUploadMaximumSizeMB: 100,
- maxNumberOfConcurrentUploadingFileChunks: 5,
- multipartUploadChunkSizeByte: 1048576, // 1MB
defaultDataTransferBatchSize: 100,
workflowEmailNotificationEnabled: false,
sharingComputingUnitEnabled: false,
diff --git a/core/gui/src/app/common/type/gui-config.ts
b/core/gui/src/app/common/type/gui-config.ts
index 4da5d98131..d60ba1cf01 100644
--- a/core/gui/src/app/common/type/gui-config.ts
+++ b/core/gui/src/app/common/type/gui-config.ts
@@ -33,9 +33,6 @@ export interface GuiConfig {
timetravelEnabled: boolean;
productionSharedEditingServer: boolean;
pythonLanguageServerPort: string;
- singleFileUploadMaximumSizeMB: number;
- maxNumberOfConcurrentUploadingFileChunks: number;
- multipartUploadChunkSizeByte: number;
defaultDataTransferBatchSize: number;
workflowEmailNotificationEnabled: boolean;
sharingComputingUnitEnabled: boolean;
diff --git
a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.html
b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.html
index 0951a81b26..a158389102 100644
---
a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.html
+++
b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.html
@@ -238,3 +238,52 @@
</div>
</div>
</nz-card>
+
+<nz-card nzTitle="Dataset">
+ <div class="settings-row">
+ <span>File Size:</span>
+ <nz-input-number
+ [(ngModel)]="maxFileSizeMB"
+ [nzMin]="1"
+ [nzStep]="10">
+ </nz-input-number>
+ </div>
+ <div class="help-text-number">Maximum size allowed for individual file
uploads (MB).</div>
+
+ <div class="settings-row">
+ <span>Concurrent Parts:</span>
+ <nz-input-number
+ [(ngModel)]="maxConcurrentChunks"
+ [nzMin]="1"
+ [nzStep]="1">
+ </nz-input-number>
+ </div>
+ <div class="help-text-number">
+ Number of file chunks that can be uploaded simultaneously. Higher values
use more memory.
+ </div>
+
+ <div class="settings-row">
+ <span>Part Size:</span>
+ <nz-input-number
+ [(ngModel)]="chunkSizeMB"
+ [nzMin]="1"
+ [nzStep]="10">
+ </nz-input-number>
+ </div>
+ <div class="help-text-number">Size of each chunk during multipart upload in
MB. Larger chunks use more memory.</div>
+
+ <div class="button-row">
+ <button
+ nz-button
+ nzType="primary"
+ (click)="saveDatasetSettings()">
+ Save
+ </button>
+ <button
+ nz-button
+ nzType="default"
+ (click)="resetDatasetSettings()">
+ Reset
+ </button>
+ </div>
+</nz-card>
diff --git
a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.scss
b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.scss
index ae34fe6405..073d6352aa 100644
---
a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.scss
+++
b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.scss
@@ -77,3 +77,17 @@ details[open] .arrow {
margin-left: 2rem;
margin-bottom: 8px;
}
+
+.settings-row {
+ display: grid;
+ grid-template-columns: 130px 320px;
+ column-gap: 6px;
+ margin-bottom: 4px;
+}
+
+.help-text-number {
+ margin-left: 140px;
+ font-size: 12px;
+ color: #888;
+ margin-bottom: 20px;
+}
diff --git
a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
index 773dd9474f..9633bb19b6 100644
---
a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
+++
b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
@@ -22,6 +22,7 @@ import { AdminSettingsService } from
"../../../service/admin/settings/admin-sett
import { NzMessageService } from "ng-zorro-antd/message";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { SidebarTabs } from "../../../../common/type/gui-config";
+import { forkJoin } from "rxjs";
@UntilDestroy()
@Component({
@@ -47,12 +48,17 @@ export class AdminSettingsComponent implements OnInit {
about_enabled: false,
};
+ maxFileSizeMB: number = 20;
+ maxConcurrentChunks: number = 10;
+ chunkSizeMB: number = 50;
+
constructor(
private adminSettingsService: AdminSettingsService,
private message: NzMessageService
) {}
ngOnInit(): void {
this.loadTabs();
+ this.loadDatasetSetting();
}
private loadTabs(): void {
@@ -64,6 +70,21 @@ export class AdminSettingsComponent implements OnInit {
});
}
+ private loadDatasetSetting(): void {
+ this.adminSettingsService
+ .getSetting("single_file_upload_max_size_mb")
+ .pipe(untilDestroyed(this))
+ .subscribe(value => (this.maxFileSizeMB = parseInt(value)));
+ this.adminSettingsService
+ .getSetting("max_number_of_concurrent_uploading_file_chunks")
+ .pipe(untilDestroyed(this))
+ .subscribe(value => (this.maxConcurrentChunks = parseInt(value)));
+ this.adminSettingsService
+ .getSetting("multipart_upload_chunk_size_mb")
+ .pipe(untilDestroyed(this))
+ .subscribe(value => (this.chunkSizeMB = parseInt(value)));
+ }
+
onFileChange(type: "logo" | "mini_logo" | "favicon", event: Event): void {
const file = (event.target as HTMLInputElement).files?.[0];
if (file && file.type.startsWith("image/")) {
@@ -181,4 +202,38 @@ export class AdminSettingsComponent implements OnInit {
this.message.info("Resetting tabs...");
setTimeout(() => window.location.reload(), 500);
}
+
+ saveDatasetSettings(): void {
+ if (this.maxFileSizeMB < 1 || this.maxConcurrentChunks < 1 ||
this.chunkSizeMB < 1) {
+ this.message.info("Value must be at least 1.");
+ return;
+ }
+
+ const saveRequests = [
+
this.adminSettingsService.updateSetting("single_file_upload_max_size_mb",
this.maxFileSizeMB.toString()),
+ this.adminSettingsService.updateSetting(
+ "max_number_of_concurrent_uploading_file_chunks",
+ this.maxConcurrentChunks.toString()
+ ),
+
this.adminSettingsService.updateSetting("multipart_upload_chunk_size_mb",
this.chunkSizeMB.toString()),
+ ];
+
+ forkJoin(saveRequests)
+ .pipe(untilDestroyed(this))
+ .subscribe({
+ next: () => this.message.success("Dataset upload settings saved
successfully."),
+ error: () => this.message.error("Failed to save dataset settings."),
+ });
+ }
+
+ resetDatasetSettings(): void {
+ [
+ "single_file_upload_max_size_mb",
+ "max_number_of_concurrent_uploading_file_chunks",
+ "multipart_upload_chunk_size_mb",
+ ].forEach(setting =>
this.adminSettingsService.resetSetting(setting).pipe(untilDestroyed(this)).subscribe({}));
+
+ this.message.info("Resetting dataset settings...");
+ setTimeout(() => window.location.reload(), 500);
+ }
}
diff --git
a/core/gui/src/app/dashboard/component/user/files-uploader/files-uploader.component.ts
b/core/gui/src/app/dashboard/component/user/files-uploader/files-uploader.component.ts
index eca4f203f7..c9bec24c60 100644
---
a/core/gui/src/app/dashboard/component/user/files-uploader/files-uploader.component.ts
+++
b/core/gui/src/app/dashboard/component/user/files-uploader/files-uploader.component.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Component, EventEmitter, Input, Output } from "@angular/core";
+import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { FileUploadItem } from "../../../type/dashboard-file.interface";
import { NgxFileDropEntry } from "ngx-file-drop";
import {
@@ -26,8 +26,10 @@ import {
getPathsUnderOrEqualDatasetFileNode,
} from "../../../../common/type/datasetVersionFileTree";
import { NotificationService } from
"../../../../common/service/notification/notification.service";
-import { GuiConfigService } from
"../../../../common/service/gui-config.service";
+import { AdminSettingsService } from
"../../../service/admin/settings/admin-settings.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+@UntilDestroy()
@Component({
selector: "texera-user-files-uploader",
templateUrl: "./files-uploader.component.html",
@@ -46,11 +48,17 @@ export class FilesUploaderComponent {
// four types: "success", "info", "warning" and "error"
fileUploadBannerType: "error" | "success" | "info" | "warning" = "success";
fileUploadBannerMessage: string = "";
+ singleFileUploadMaxSizeMB: number = 20;
constructor(
private notificationService: NotificationService,
- private config: GuiConfigService
- ) {}
+ private adminSettingsService: AdminSettingsService
+ ) {
+ this.adminSettingsService
+ .getSetting("single_file_upload_max_size_mb")
+ .pipe(untilDestroyed(this))
+ .subscribe(value => (this.singleFileUploadMaxSizeMB = parseInt(value)));
+ }
hideBanner() {
this.fileUploadingFinished = false;
@@ -71,10 +79,10 @@ export class FilesUploaderComponent {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
fileEntry.file(file => {
// Check the file size here
- if (file.size > this.config.env.singleFileUploadMaximumSizeMB *
1024 * 1024) {
+ if (file.size > this.singleFileUploadMaxSizeMB * 1024 * 1024) {
// If the file is too large, reject the promise
this.notificationService.error(
- `File ${file.name}'s size exceeds the maximum limit of
${this.config.env.singleFileUploadMaximumSizeMB}MB.`
+ `File ${file.name}'s size exceeds the maximum limit of
${this.singleFileUploadMaxSizeMB}MB.`
);
reject(null);
} else {
diff --git
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
index 317604dfda..9dd4de0c40 100644
---
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
+++
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
@@ -39,6 +39,7 @@ import { FileUploadItem } from
"../../../../type/dashboard-file.interface";
import { DatasetStagedObject } from
"../../../../../common/type/dataset-staged-object";
import { NzModalService } from "ng-zorro-antd/modal";
import { UserDatasetVersionCreatorComponent } from
"./user-dataset-version-creator/user-dataset-version-creator.component";
+import { AdminSettingsService } from
"../../../../service/admin/settings/admin-settings.service";
export const THROTTLE_TIME_MS = 1000;
@@ -76,6 +77,9 @@ export class DatasetDetailComponent implements OnInit {
public displayPreciseViewCount = false;
userHasPendingChanges: boolean = false;
+ // Uploading setting
+ chunkSizeMB: number = 50;
+ maxConcurrentChunks: number = 10;
// List of upload tasks – each task tracked by its filePath
public uploadTasks: Array<
@@ -94,7 +98,8 @@ export class DatasetDetailComponent implements OnInit {
private notificationService: NotificationService,
private downloadService: DownloadService,
private userService: UserService,
- private hubService: HubService
+ private hubService: HubService,
+ private adminSettingsService: AdminSettingsService
) {
this.userService
.userChanged()
@@ -159,8 +164,9 @@ export class DatasetDetailComponent implements OnInit {
.subscribe((isLiked: LikedStatus[]) => {
this.isLiked = isLiked.length > 0 ? isLiked[0].isLiked : false;
});
- }
+ this.loadUploadSettings();
+ }
public onClickOpenVersionCreator() {
if (this.did) {
const modal = this.modalService.create({
@@ -309,6 +315,17 @@ export class DatasetDetailComponent implements OnInit {
return task.filePath;
}
+ private loadUploadSettings(): void {
+ this.adminSettingsService
+ .getSetting("multipart_upload_chunk_size_mb")
+ .pipe(untilDestroyed(this))
+ .subscribe(value => (this.chunkSizeMB = parseInt(value)));
+ this.adminSettingsService
+ .getSetting("max_number_of_concurrent_uploading_file_chunks")
+ .pipe(untilDestroyed(this))
+ .subscribe(value => (this.maxConcurrentChunks = parseInt(value)));
+ }
+
onNewUploadFilesChanged(files: FileUploadItem[]) {
if (this.did) {
files.forEach((file, idx) => {
@@ -320,10 +337,15 @@ export class DatasetDetailComponent implements OnInit {
uploadId: "",
physicalAddress: "",
});
-
// Start multipart upload
this.datasetService
- .multipartUpload(this.datasetName, file.name, file.file)
+ .multipartUpload(
+ this.datasetName,
+ file.name,
+ file.file,
+ this.chunkSizeMB * 1024 * 1024,
+ this.maxConcurrentChunks
+ )
.pipe(untilDestroyed(this))
.subscribe({
next: progress => {
diff --git a/core/gui/src/app/dashboard/service/user/dataset/dataset.service.ts
b/core/gui/src/app/dashboard/service/user/dataset/dataset.service.ts
index 29bc627cd8..226b1edb97 100644
--- a/core/gui/src/app/dashboard/service/user/dataset/dataset.service.ts
+++ b/core/gui/src/app/dashboard/service/user/dataset/dataset.service.ts
@@ -137,10 +137,14 @@ export class DatasetService {
* Handles multipart upload for large files using RxJS,
* with a concurrency limit on how many parts we process in parallel.
*/
- public multipartUpload(datasetName: string, filePath: string, file: File):
Observable<MultipartUploadProgress> {
- const partSize = this.config.env.multipartUploadChunkSizeByte;
+ public multipartUpload(
+ datasetName: string,
+ filePath: string,
+ file: File,
+ partSize: number,
+ concurrencyLimit: number
+ ): Observable<MultipartUploadProgress> {
const partCount = Math.ceil(file.size / partSize);
- const concurrencyLimit =
this.config.env.maxNumberOfConcurrentUploadingFileChunks;
// track progress bar
let totalBytesUploaded = 0;