Skip to main content

wekan_cli/commands/admin/
attachment_settings.rs

1use super::input;
2use crate::client::admin::{
3    BatchSize, CpuThreshold, DelayMs, GcsCredentials, NonNegativeNumber, PreparedSettingsUpdate,
4    SettingNumber, StorageSecret, attachment_settings::*,
5};
6use crate::{
7    client::WekanClientFactory,
8    command_result::*,
9    commands::{
10        authenticated::AuthenticatedContext,
11        client_error::{map_client_error, map_client_error_with_not_found},
12    },
13    credentials::{CredentialStore, SecretInputProvider},
14    error::AppError,
15    redaction::Redactor,
16};
17use clap::{ArgGroup, Args, Subcommand};
18#[derive(Debug, Args)]
19pub struct AttachmentSettingsArgs {
20    #[command(subcommand)]
21    pub command: AttachmentSettingsCommand,
22}
23#[derive(Debug, Subcommand)]
24pub enum AttachmentSettingsCommand {
25    /// Return the complete mapped settings document.
26    Get,
27    /// Apply native settings updates, with no read-before-write merging.
28    Update(Box<UpdateArgs>),
29}
30#[derive(Debug, Args)]
31#[command(group(ArgGroup::new("credential_stdin").args([
32    "s3_secret_access_key_stdin",
33    "azure_account_key_stdin",
34    "azure_connection_string_stdin",
35    "gcs_credentials_stdin",
36    "webdav_password_stdin",
37]).multiple(false)), group(ArgGroup::new("setting_flags").args([
38    "webdav_enabled",
39    "webdav_read",
40    "webdav_write",
41    "webdav_endpoint",
42    "webdav_username",
43    "webdav_bucket",
44    "webdav_password_file",
45    "webdav_password_stdin",
46    "default_storage",
47    "filesystem_enabled",
48    "filesystem_read",
49    "filesystem_write",
50    "filesystem_path",
51    "gridfs_enabled",
52    "gridfs_read",
53    "gridfs_write",
54    "s3_enabled",
55    "s3_read",
56    "s3_write",
57    "s3_endpoint",
58    "s3_region",
59    "s3_bucket",
60    "s3_access_key_id",
61    "s3_force_path_style",
62    "azure_enabled",
63    "azure_read",
64    "azure_write",
65    "azure_account_name",
66    "azure_bucket",
67    "gcs_enabled",
68    "gcs_read",
69    "gcs_write",
70    "gcs_project_id",
71    "gcs_key_filename",
72    "gcs_bucket",
73    "upload_max_file_size",
74    "upload_allowed_mime_types",
75    "attachments_upload_max_bytes",
76    "attachments_upload_blocked",
77    "avatars_upload_blocked",
78    "attachments_download_max_bytes",
79    "attachments_download_blocked",
80    "api_upload_max_bytes",
81    "api_upload_blocked",
82    "api_download_max_bytes",
83    "api_download_blocked",
84    "migration_auto_migrate",
85    "migration_batch_size",
86    "migration_delay_ms",
87    "migration_cpu_threshold",
88    "s3_secret_access_key_file",
89    "s3_secret_access_key_stdin",
90    "azure_account_key_file",
91    "azure_account_key_stdin",
92    "azure_connection_string_file",
93    "azure_connection_string_stdin",
94    "gcs_credentials_file",
95    "gcs_credentials_stdin",
96    "clear_upload_allowed_mime_types",
97]).multiple(true).conflicts_with("settings_source")), group(ArgGroup::new("update_input").args([
98    "settings_file",
99    "settings_stdin",
100    "webdav_enabled",
101    "webdav_read",
102    "webdav_write",
103    "webdav_endpoint",
104    "webdav_username",
105    "webdav_bucket",
106    "webdav_password_file",
107    "webdav_password_stdin",
108    "default_storage",
109    "filesystem_enabled",
110    "filesystem_read",
111    "filesystem_write",
112    "filesystem_path",
113    "gridfs_enabled",
114    "gridfs_read",
115    "gridfs_write",
116    "s3_enabled",
117    "s3_read",
118    "s3_write",
119    "s3_endpoint",
120    "s3_region",
121    "s3_bucket",
122    "s3_access_key_id",
123    "s3_force_path_style",
124    "azure_enabled",
125    "azure_read",
126    "azure_write",
127    "azure_account_name",
128    "azure_bucket",
129    "gcs_enabled",
130    "gcs_read",
131    "gcs_write",
132    "gcs_project_id",
133    "gcs_key_filename",
134    "gcs_bucket",
135    "upload_max_file_size",
136    "upload_allowed_mime_types",
137    "attachments_upload_max_bytes",
138    "attachments_upload_blocked",
139    "avatars_upload_blocked",
140    "attachments_download_max_bytes",
141    "attachments_download_blocked",
142    "api_upload_max_bytes",
143    "api_upload_blocked",
144    "api_download_max_bytes",
145    "api_download_blocked",
146    "migration_auto_migrate",
147    "migration_batch_size",
148    "migration_delay_ms",
149    "migration_cpu_threshold",
150    "s3_secret_access_key_file",
151    "s3_secret_access_key_stdin",
152    "azure_account_key_file",
153    "azure_account_key_stdin",
154    "azure_connection_string_file",
155    "azure_connection_string_stdin",
156    "gcs_credentials_file",
157    "gcs_credentials_stdin",
158    "clear_upload_allowed_mime_types",
159]).required(true).multiple(true)), after_help = "Nested sections use Wekan replacement/default semantics: omitted siblings can reset or disappear. Cloud providers merge and blank secrets preserve stored credentials. Saving does not verify connectivity. JSON sources conflict with every setting flag.")]
160pub struct UpdateArgs {
161    #[command(flatten)]
162    source: input::SourceArgs,
163    /// Set WebDAV enabled.
164    #[arg(long)]
165    webdav_enabled: Option<bool>,
166    /// Set WebDAV read.
167    #[arg(long)]
168    webdav_read: Option<bool>,
169    /// Set WebDAV write.
170    #[arg(long)]
171    webdav_write: Option<bool>,
172    /// Set WebDAV endpoint.
173    #[arg(long)]
174    webdav_endpoint: Option<String>,
175    /// Set WebDAV username.
176    #[arg(long)]
177    webdav_username: Option<String>,
178    /// Set WebDAV bucket.
179    #[arg(long)]
180    webdav_bucket: Option<String>,
181    /// Read the WebDAV password from a regular file.
182    #[arg(long, conflicts_with = "webdav_password_stdin")]
183    webdav_password_file: Option<std::path::PathBuf>,
184    /// Read the WebDAV password securely from stdin.
185    #[arg(long, conflicts_with = "webdav_password_file")]
186    webdav_password_stdin: bool,
187
188    /// Default backend: fs, gridfs, s3, azure, gcs, or webdav.
189    #[arg(long = "default-storage")]
190    default_storage: Option<StorageBackend>,
191    /// Set filesystem enabled. Explicit true or false.
192    #[arg(long = "filesystem-enabled")]
193    filesystem_enabled: Option<bool>,
194    /// Set filesystem read. Explicit true or false.
195    #[arg(long = "filesystem-read")]
196    filesystem_read: Option<bool>,
197    /// Set filesystem write. Explicit true or false.
198    #[arg(long = "filesystem-write")]
199    filesystem_write: Option<bool>,
200    /// Set filesystem path.
201    #[arg(long = "filesystem-path")]
202    filesystem_path: Option<String>,
203    /// Set gridfs enabled. Explicit true or false.
204    #[arg(long = "gridfs-enabled")]
205    gridfs_enabled: Option<bool>,
206    /// Set gridfs read. Explicit true or false.
207    #[arg(long = "gridfs-read")]
208    gridfs_read: Option<bool>,
209    /// Set gridfs write. Explicit true or false.
210    #[arg(long = "gridfs-write")]
211    gridfs_write: Option<bool>,
212    /// Set s3 enabled. Explicit true or false.
213    #[arg(long = "s3-enabled")]
214    s3_enabled: Option<bool>,
215    /// Set s3 read. Explicit true or false.
216    #[arg(long = "s3-read")]
217    s3_read: Option<bool>,
218    /// Set s3 write. Explicit true or false.
219    #[arg(long = "s3-write")]
220    s3_write: Option<bool>,
221    /// Set s3 endpoint.
222    #[arg(long = "s3-endpoint")]
223    s3_endpoint: Option<String>,
224    /// Set s3 region.
225    #[arg(long = "s3-region")]
226    s3_region: Option<String>,
227    /// Set s3 bucket.
228    #[arg(long = "s3-bucket")]
229    s3_bucket: Option<String>,
230    /// Set s3 access key id.
231    #[arg(long = "s3-access-key-id")]
232    s3_access_key_id: Option<String>,
233    /// Set s3 force path style. Explicit true or false.
234    #[arg(long = "s3-force-path-style")]
235    s3_force_path_style: Option<bool>,
236    /// Set azure enabled. Explicit true or false.
237    #[arg(long = "azure-enabled")]
238    azure_enabled: Option<bool>,
239    /// Set azure read. Explicit true or false.
240    #[arg(long = "azure-read")]
241    azure_read: Option<bool>,
242    /// Set azure write. Explicit true or false.
243    #[arg(long = "azure-write")]
244    azure_write: Option<bool>,
245    /// Set azure account name.
246    #[arg(long = "azure-account-name")]
247    azure_account_name: Option<String>,
248    /// Set azure bucket.
249    #[arg(long = "azure-bucket")]
250    azure_bucket: Option<String>,
251    /// Set gcs enabled. Explicit true or false.
252    #[arg(long = "gcs-enabled")]
253    gcs_enabled: Option<bool>,
254    /// Set gcs read. Explicit true or false.
255    #[arg(long = "gcs-read")]
256    gcs_read: Option<bool>,
257    /// Set gcs write. Explicit true or false.
258    #[arg(long = "gcs-write")]
259    gcs_write: Option<bool>,
260    /// Set gcs project id.
261    #[arg(long = "gcs-project-id")]
262    gcs_project_id: Option<String>,
263    /// Set gcs key filename.
264    #[arg(long = "gcs-key-filename")]
265    gcs_key_filename: Option<String>,
266    /// Set gcs bucket.
267    #[arg(long = "gcs-bucket")]
268    gcs_bucket: Option<String>,
269    /// Set upload max file size.
270    #[arg(long = "upload-max-file-size", allow_hyphen_values = true)]
271    upload_max_file_size: Option<SettingNumber>,
272    /// Replace allowed MIME types; repeat for each entry.
273    #[arg(
274        long = "upload-allowed-mime-type",
275        conflicts_with = "clear_upload_allowed_mime_types"
276    )]
277    upload_allowed_mime_types: Vec<String>,
278    /// Set attachments upload max bytes.
279    #[arg(long = "attachments-upload-max-bytes", allow_hyphen_values = true)]
280    attachments_upload_max_bytes: Option<NonNegativeNumber>,
281    /// Set attachments upload blocked. Explicit true or false.
282    #[arg(long = "attachments-upload-blocked")]
283    attachments_upload_blocked: Option<bool>,
284    /// Set avatars upload blocked. Explicit true or false.
285    #[arg(long = "avatars-upload-blocked")]
286    avatars_upload_blocked: Option<bool>,
287    /// Set attachments download max bytes.
288    #[arg(long = "attachments-download-max-bytes", allow_hyphen_values = true)]
289    attachments_download_max_bytes: Option<NonNegativeNumber>,
290    /// Set attachments download blocked. Explicit true or false.
291    #[arg(long = "attachments-download-blocked")]
292    attachments_download_blocked: Option<bool>,
293    /// Set api upload max bytes.
294    #[arg(long = "api-upload-max-bytes", allow_hyphen_values = true)]
295    api_upload_max_bytes: Option<NonNegativeNumber>,
296    /// Set api upload blocked. Explicit true or false.
297    #[arg(long = "api-upload-blocked")]
298    api_upload_blocked: Option<bool>,
299    /// Set api download max bytes.
300    #[arg(long = "api-download-max-bytes", allow_hyphen_values = true)]
301    api_download_max_bytes: Option<NonNegativeNumber>,
302    /// Set api download blocked. Explicit true or false.
303    #[arg(long = "api-download-blocked")]
304    api_download_blocked: Option<bool>,
305    /// Set migration auto migrate. Explicit true or false.
306    #[arg(long = "migration-auto-migrate")]
307    migration_auto_migrate: Option<bool>,
308    /// Set migration batch size.
309    #[arg(long = "migration-batch-size", allow_hyphen_values = true)]
310    migration_batch_size: Option<BatchSize>,
311    /// Set migration delay ms.
312    #[arg(long = "migration-delay-ms", allow_hyphen_values = true)]
313    migration_delay_ms: Option<DelayMs>,
314    /// Set migration cpu threshold.
315    #[arg(long = "migration-cpu-threshold", allow_hyphen_values = true)]
316    migration_cpu_threshold: Option<CpuThreshold>,
317    /// Read a storage credential from a regular UTF-8 file; blank preserves the stored secret.
318    #[arg(
319        long = "s3-secret-access-key-file",
320        conflicts_with = "s3_secret_access_key_stdin",
321        value_name = "PATH"
322    )]
323    s3_secret_access_key_file: Option<std::path::PathBuf>,
324    /// Read a storage credential from UTF-8 stdin; blank preserves the stored secret.
325    #[arg(
326        long = "s3-secret-access-key-stdin",
327        conflicts_with = "s3_secret_access_key_file"
328    )]
329    s3_secret_access_key_stdin: bool,
330    /// Read a storage credential from a regular UTF-8 file; blank preserves the stored secret.
331    #[arg(
332        long = "azure-account-key-file",
333        conflicts_with = "azure_account_key_stdin",
334        value_name = "PATH"
335    )]
336    azure_account_key_file: Option<std::path::PathBuf>,
337    /// Read a storage credential from UTF-8 stdin; blank preserves the stored secret.
338    #[arg(
339        long = "azure-account-key-stdin",
340        conflicts_with = "azure_account_key_file"
341    )]
342    azure_account_key_stdin: bool,
343    /// Read a storage credential from a regular UTF-8 file; blank preserves the stored secret.
344    #[arg(
345        long = "azure-connection-string-file",
346        conflicts_with = "azure_connection_string_stdin",
347        value_name = "PATH"
348    )]
349    azure_connection_string_file: Option<std::path::PathBuf>,
350    /// Read a storage credential from UTF-8 stdin; blank preserves the stored secret.
351    #[arg(
352        long = "azure-connection-string-stdin",
353        conflicts_with = "azure_connection_string_file"
354    )]
355    azure_connection_string_stdin: bool,
356    /// Read a storage credential from a regular UTF-8 file; blank preserves the stored secret.
357    #[arg(
358        long = "gcs-credentials-file",
359        conflicts_with = "gcs_credentials_stdin",
360        value_name = "PATH"
361    )]
362    gcs_credentials_file: Option<std::path::PathBuf>,
363    /// Read a storage credential from UTF-8 stdin; blank preserves the stored secret.
364    #[arg(
365        long = "gcs-credentials-stdin",
366        conflicts_with = "gcs_credentials_file"
367    )]
368    gcs_credentials_stdin: bool,
369    /// Replace the allowed MIME type list with an empty array.
370    #[arg(
371        long = "clear-upload-allowed-mime-types",
372        conflicts_with = "upload_allowed_mime_types"
373    )]
374    clear_upload_allowed_mime_types: bool,
375}
376impl UpdateArgs {
377    async fn request(
378        self,
379        secrets: &dyn SecretInputProvider,
380    ) -> Result<UpdateAttachmentSettings, AppError> {
381        if let Some(request) = self.source.read().await? {
382            return Ok(request);
383        }
384        let s3_secret_access_key = input::secret(
385            self.s3_secret_access_key_file,
386            self.s3_secret_access_key_stdin,
387            secrets,
388        )
389        .await?
390        .map(StorageSecret);
391        let azure_account_key = input::secret(
392            self.azure_account_key_file,
393            self.azure_account_key_stdin,
394            secrets,
395        )
396        .await?
397        .map(StorageSecret);
398        let azure_connection_string = input::secret(
399            self.azure_connection_string_file,
400            self.azure_connection_string_stdin,
401            secrets,
402        )
403        .await?
404        .map(StorageSecret);
405        let gcs_credentials = input::secret(
406            self.gcs_credentials_file,
407            self.gcs_credentials_stdin,
408            secrets,
409        )
410        .await?
411        .map(GcsCredentials::text);
412        let webdav_password = input::secret(
413            self.webdav_password_file,
414            self.webdav_password_stdin,
415            secrets,
416        )
417        .await?
418        .map(StorageSecret);
419        let webdav = WebdavUpdate {
420            enabled: self.webdav_enabled,
421            read: self.webdav_read,
422            write: self.webdav_write,
423            endpoint: self.webdav_endpoint,
424            username: self.webdav_username,
425            bucket: self.webdav_bucket,
426            password: webdav_password,
427        };
428        let webdav = if webdav.enabled.is_some()
429            || webdav.read.is_some()
430            || webdav.write.is_some()
431            || webdav.endpoint.is_some()
432            || webdav.username.is_some()
433            || webdav.bucket.is_some()
434            || webdav.password.is_some()
435        {
436            Some(webdav)
437        } else {
438            None
439        };
440        let filesystem = FilesystemUpdate {
441            enabled: self.filesystem_enabled,
442            read: self.filesystem_read,
443            write: self.filesystem_write,
444            path: self.filesystem_path,
445        };
446        let filesystem = if filesystem.enabled.is_some()
447            || filesystem.read.is_some()
448            || filesystem.write.is_some()
449            || filesystem.path.is_some()
450        {
451            Some(filesystem)
452        } else {
453            None
454        };
455        let gridfs = GridfsUpdate {
456            enabled: self.gridfs_enabled,
457            read: self.gridfs_read,
458            write: self.gridfs_write,
459        };
460        let gridfs = if gridfs.enabled.is_some() || gridfs.read.is_some() || gridfs.write.is_some()
461        {
462            Some(gridfs)
463        } else {
464            None
465        };
466        let s3 = S3Update {
467            enabled: self.s3_enabled,
468            read: self.s3_read,
469            write: self.s3_write,
470            endpoint: self.s3_endpoint,
471            region: self.s3_region,
472            bucket: self.s3_bucket,
473            access_key_id: self.s3_access_key_id,
474            force_path_style: self.s3_force_path_style,
475            secret_access_key: s3_secret_access_key,
476        };
477        let s3 = if s3.enabled.is_some()
478            || s3.read.is_some()
479            || s3.write.is_some()
480            || s3.endpoint.is_some()
481            || s3.region.is_some()
482            || s3.bucket.is_some()
483            || s3.access_key_id.is_some()
484            || s3.force_path_style.is_some()
485            || s3.secret_access_key.is_some()
486        {
487            Some(s3)
488        } else {
489            None
490        };
491        let azure = AzureUpdate {
492            enabled: self.azure_enabled,
493            read: self.azure_read,
494            write: self.azure_write,
495            account_name: self.azure_account_name,
496            bucket: self.azure_bucket,
497            account_key: azure_account_key,
498            connection_string: azure_connection_string,
499        };
500        let azure = if azure.enabled.is_some()
501            || azure.read.is_some()
502            || azure.write.is_some()
503            || azure.account_name.is_some()
504            || azure.bucket.is_some()
505            || azure.account_key.is_some()
506            || azure.connection_string.is_some()
507        {
508            Some(azure)
509        } else {
510            None
511        };
512        let gcs = GcsUpdate {
513            enabled: self.gcs_enabled,
514            read: self.gcs_read,
515            write: self.gcs_write,
516            project_id: self.gcs_project_id,
517            key_filename: self.gcs_key_filename,
518            bucket: self.gcs_bucket,
519            credentials: gcs_credentials,
520        };
521        let gcs = if gcs.enabled.is_some()
522            || gcs.read.is_some()
523            || gcs.write.is_some()
524            || gcs.project_id.is_some()
525            || gcs.key_filename.is_some()
526            || gcs.bucket.is_some()
527            || gcs.credentials.is_some()
528        {
529            Some(gcs)
530        } else {
531            None
532        };
533        let upload = UploadUpdate {
534            max_file_size: self.upload_max_file_size,
535            allowed_mime_types: if self.clear_upload_allowed_mime_types {
536                Some(Vec::new())
537            } else if self.upload_allowed_mime_types.is_empty() {
538                None
539            } else {
540                Some(self.upload_allowed_mime_types)
541            },
542        };
543        let upload = if upload.max_file_size.is_some() || upload.allowed_mime_types.is_some() {
544            Some(upload)
545        } else {
546            None
547        };
548        let limits = LimitsUpdate {
549            attachments_upload_max_bytes: self.attachments_upload_max_bytes,
550            attachments_upload_blocked: self.attachments_upload_blocked,
551            avatars_upload_blocked: self.avatars_upload_blocked,
552            attachments_download_max_bytes: self.attachments_download_max_bytes,
553            attachments_download_blocked: self.attachments_download_blocked,
554            api_upload_max_bytes: self.api_upload_max_bytes,
555            api_upload_blocked: self.api_upload_blocked,
556            api_download_max_bytes: self.api_download_max_bytes,
557            api_download_blocked: self.api_download_blocked,
558        };
559        let limits = if limits.attachments_upload_max_bytes.is_some()
560            || limits.attachments_upload_blocked.is_some()
561            || limits.avatars_upload_blocked.is_some()
562            || limits.attachments_download_max_bytes.is_some()
563            || limits.attachments_download_blocked.is_some()
564            || limits.api_upload_max_bytes.is_some()
565            || limits.api_upload_blocked.is_some()
566            || limits.api_download_max_bytes.is_some()
567            || limits.api_download_blocked.is_some()
568        {
569            Some(limits)
570        } else {
571            None
572        };
573        let migration = MigrationUpdate {
574            auto_migrate: self.migration_auto_migrate,
575            batch_size: self.migration_batch_size,
576            delay_ms: self.migration_delay_ms,
577            cpu_threshold: self.migration_cpu_threshold,
578        };
579        let migration = if migration.auto_migrate.is_some()
580            || migration.batch_size.is_some()
581            || migration.delay_ms.is_some()
582            || migration.cpu_threshold.is_some()
583        {
584            Some(migration)
585        } else {
586            None
587        };
588        let storage_config = StorageConfigUpdate {
589            webdav,
590            filesystem,
591            gridfs,
592            s3,
593            azure,
594            gcs,
595        };
596        let has_config = storage_config.webdav.is_some()
597            || storage_config.filesystem.is_some()
598            || storage_config.gridfs.is_some()
599            || storage_config.s3.is_some()
600            || storage_config.azure.is_some()
601            || storage_config.gcs.is_some();
602        Ok(UpdateAttachmentSettings {
603            default_storage: self.default_storage,
604            storage_config: has_config.then_some(storage_config),
605            upload_settings: upload,
606            limit_settings: limits,
607            migration_settings: migration,
608        })
609    }
610}
611pub(crate) async fn execute(
612    command: AttachmentSettingsCommand,
613    factory: &WekanClientFactory,
614    store: &dyn CredentialStore,
615    secrets: &dyn SecretInputProvider,
616) -> Result<CommandSuccess, AppError> {
617    match command {
618        AttachmentSettingsCommand::Get => {
619            let context = AuthenticatedContext::load(factory, store)?;
620            let document = context
621                .client()
622                .attachment_settings(context.record().token())
623                .await
624                .map_err(|e| {
625                    map_client_error(
626                        e,
627                        &Redactor::with_secret(context.record().token()),
628                        "admin attachment-settings get",
629                    )
630                })?;
631            Ok(CommandSuccess::AttachmentSettings(Box::new(
632                AttachmentSettingsSuccess {
633                    settings: document.into(),
634                },
635            )))
636        }
637        AttachmentSettingsCommand::Update(args) => {
638            let request = args.request(secrets).await?;
639            let submitted_fields = submitted_fields(&request);
640            input::validate_fields(&submitted_fields)?;
641            let prepared = PreparedSettingsUpdate::new(&request)
642                .map_err(|error| AppError::invalid_input(error.to_string()))?;
643            let context = AuthenticatedContext::load(factory, store)?;
644            let secrets = request.redaction_secrets();
645            let mut redactor = Redactor::with_secret(context.record().token());
646            for secret in &secrets {
647                redactor = redactor.and_secret(secret);
648            }
649            let document = context
650                .client()
651                .update_attachment_settings(prepared, context.record().token())
652                .await
653                .map_err(|e| {
654                    map_client_error_with_not_found(
655                        e,
656                        &redactor,
657                        "admin attachment-settings update",
658                        "the Wekan settings document was not found",
659                    )
660                })?;
661            Ok(CommandSuccess::AttachmentSettingsUpdated(Box::new(
662                AttachmentSettingsUpdateSuccess {
663                    settings: document.into(),
664                    submitted_fields,
665                },
666            )))
667        }
668    }
669}
670
671fn submitted_fields(request: &UpdateAttachmentSettings) -> Vec<AttachmentSettingField> {
672    let mut fields = Vec::new();
673    if request.default_storage.is_some() {
674        fields.push(AttachmentSettingField::DefaultStorage);
675    }
676    if request.storage_config.is_some() {
677        fields.push(AttachmentSettingField::StorageConfig);
678    }
679    if request.upload_settings.is_some() {
680        fields.push(AttachmentSettingField::UploadSettings);
681    }
682    if request.limit_settings.is_some() {
683        fields.push(AttachmentSettingField::LimitSettings);
684    }
685    if request.migration_settings.is_some() {
686        fields.push(AttachmentSettingField::MigrationSettings);
687    }
688    fields
689}