0
votes

In my Angular application I have a contact form with a file upload input. The file upload input on the front-end side disallows to send a contact form if the files combined are more than 20 MB. Is there any way to achieve the same logic in Cloud Storage for Firebase? Currently I can only limit 20 MB, but for each file, i.e. if someone will upload 10 files, each 19 MB s/he won't be able to send a form, but the files will be send to my serverless back-end, which I don't want.

contact.component.html

<mat-form-field>
  <!--
    Accept only files in the following format: .doc, .docx, .jpg, .jpeg, .pdf, .png, .xls, .xlsx. However, this is easy to bypass, Cloud Storage rules has been set up on the back-end side.
  -->
  <ngx-mat-file-input
    [accept]="[
      '.doc',
      '.docx',
      '.jpg',
      '.jpeg',
      '.pdf',
      '.png',
      '.xls',
      '.xlsx'
    ]"
    (change)="uploadFile($event)"
    formControlName="fileUploader"
    multiple
    aria-label="Here you can add additional files about your project, which can be helpeful for us."
    placeholder="Additional files"
    title="Additional files"
    type="file"
  >
  </ngx-mat-file-input>
  <mat-icon matSuffix>folder</mat-icon>
  <mat-hint
    >Accepted formats: DOC, DOCX, JPG, JPEG, PDF, PNG, XLS and XLSX,
    maximum files upload size: 20 MB.
  </mat-hint>
  <!--
    Non-null assertion operators are required to let know the compiler that this value is not empty and exists.
  -->
  <mat-error
    *ngIf="contactForm.get('fileUploader')!.hasError('maxContentSize')"
  >
    This size is too large,
    <strong
      >maximum acceptable upload size is
      {{
        contactForm.get('fileUploader')?.getError('maxContentSize')
          .maxSize | byteFormat
      }}</strong
    >
    (uploaded size:
    {{
      contactForm.get('fileUploader')?.getError('maxContentSize')
        .actualSize | byteFormat
    }}).
  </mat-error>
</mat-form-field>

contact.component.ts (size validator)

public maxFileSize = 20971520;
public contactForm: FormGroup = this.formBuilder.group({
    fileUploader: [
      '',
      Validators.compose([
        FileValidator.maxContentSize(this.maxFileSize),
        Validators.maxLength(512),
        Validators.minLength(2)
      ])
    ].toString()
})

contact.component.ts (file uploader)

/**
   * @description Upload additional files to Cloud Firestore and get URL to the files.
   * @param {event} - object of sent files.
   * @returns {void}
   */
  public uploadFile(event: any): void {
    // Iterate through all uploaded files.
    for (let i = 0; i < event.target.files.length; i++) {
      const randomId = Math.random()
        .toString(36)
        .substring(2); // Create random ID, so the same file names can be uploaded to Cloud Firestore.

      const file = event.target.files[i]; // Get each uploaded file.

      // Get file reference.
      const fileRef: AngularFireStorageReference = this.angularFireStorage.ref(
        randomId
      );

      // Create upload task.
      const task: AngularFireUploadTask = this.angularFireStorage.upload(
        randomId,
        file
      );

      // Upload file to Cloud Firestore.
      task
        .snapshotChanges()
        .pipe(
          finalize(() => {
            fileRef.getDownloadURL().subscribe((downloadURL: string) => {
              this.angularFirestore
                .collection(process.env.FIRESTORE_COLLECTION_FILES!) // Non-null assertion operator is required to let know the compiler that this value is not empty and exists.
                .add({ downloadURL: downloadURL });
              this.downloadURL.push(downloadURL);
            });
          }),
          catchError((error: any) => {
            return throwError(error);
          })
        )
        .subscribe();
    }
  }

storage.rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
        allow read; // Required in order to send this as attachment.
      // Allow write files Firebase Storage, only if:
      // 1) File is no more than 20MB
      // 2) Content type is in one of the following formats: .doc, .docx, .jpg, .jpeg, .pdf, .png, .xls, .xlsx.
      allow write: if request.resource.size <= 20 * 1024 * 1024
        && (request.resource.contentType.matches('application/msword')
        || request.resource.contentType.matches('application/vnd.openxmlformats-officedocument.wordprocessingml.document')
        || request.resource.contentType.matches('image/jpg')
        || request.resource.contentType.matches('image/jpeg')
        || request.resource.contentType.matches('application/pdf')
                || request.resource.contentType.matches('image/png')
        || request.resource.contentType.matches('application/vnd.ms-excel')
        || request.resource.contentType.matches('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'))
    }
  }
}
1

1 Answers

2
votes

Limits in Cloud Storage for Firebase security rules apply to each file/object separately, they don't apply to an entire operation. In the context of Cloud Storage rules that type of limit also would make little sense, as the user could just start another operation to upload the additional files.

Some options to consider:

  1. If you hardcode the names of the files that the user uploads (which also implies you'll limit the number of files they can upload), and create a folder for the files for each specific user, you can determine the sum of all files in a user's folder, and thus limit on the sum in that way.

  2. Alternatively, you can ZIP all files together on the client, and then upload the resulting archive. In that case, the security rules can enforce the maximum size of that file.

  3. And of course you can include client-side JavaScript code to check the maximum size of the combined files in both of these cases. A malicious user can bypass this JavaScript easily, but most users aren't malicious and will thank you for saving their bandwidth by preventing the upload that will be rejected anyway.

  4. You can also use a HTTPS Cloud Function as your upload target, and then only pass the files onto Cloud Storage if they meet your requirements.

  5. Alternatively you can use a Cloud Function that triggers upon the upload from the user, and validates the files for that user after the change. At that point you can correct the situation.

Many of these scenarios require (or work better) that you have a structure where you can see from the path which user uploaded each file. See the documentation on user-private files for more on this structure.