angularlaravelamazon-s3

How to cancel a file upload to S3 using Angular/Laravel?


I'm building a file upload with Angular and Laravel. I want to be able to cancel a file upload while it's in progress. So far it's working, but I'm not too confident so I want to make sure I'm understanding the logic correctly. If not, find a better solution.

So far, I have an Angular component for the file upload that will make an HTTP POST request to my API to upload the file using the laravel Storage facade and putFileAs().

From my understanding (I'm new to Laravel and Angular), I'm streaming the file to my backend and streaming the upload to S3 using putFileAs(). So if I want to cancel the upload to S3, I just need to unsubscribe from the observable in my angular component.

Is my understanding correct? Any holes in my logic?

Angular functions that handle the upload

uploadFile(file: File){
  this.progressBar = true;
  this.uploading = this.fileService.uploadFile(file)
    .pipe(finalize((res) => {
      console.log('Upload success!');
    })
    .subscribe(res => {
      this.progressBar = false;
    });
}


cancelUpload(){
  if(this.uploading){
    this.uploading.unsubscribe();
  }
  this.progressBar = false;
}

Controller function that handles the upload.

public function uploadFile(Request $request){
  $data = $request->input();
  $file = $request->file('file');

  $res = Storage::disk('s3')->putFileAs('/somePath/', $file, $data['filename']);

  if($res){
    return response()->json($res, 200);
  } else {
    return response()->json($res, 422);
  }
}

Solution

  • I think the Angular portion of this logic is correct, but may I suggest the following without knowing how the FileUploadService class works. Key considerations here is using subject to cancel the request, and the addition of reportProgress from httpClient.

    For Laravel, maybe add a try catch block to catch exceptions, and possibly validations to check validity of the file, since the Angular code itself might not have those validations.

    FileUploadService

    export class FileUploadService {
      private cancelUpload$ = new Subject<void>();
    
      constructor(private http: HttpClient) {}
    
      // File upload function with progress tracker
      uploadFile(file: File): Observable<any> {
        const formData = new FormData();
        formData.append('file', file);
    
        const headers = new HttpHeaders();
        const url = 'test-endpoint'; 
    
        return this.http.post(url, formData, {
          headers: headers,
          reportProgress: true,
          observe: 'events',
        }).pipe(
          takeUntil(this.cancelUpload$)
        );
      }
    
      cancelUpload() {
        this.cancelUpload$.next();
      }
    }
    

    Component

    export Class Component {
          uploadProgress: number = 0;
          uploading: boolean = false;
          uploadSubscription: any;
          fileUploadService = inject(FileUploadService);
    
          onUpload(file: File) {
            this.uploading = true;
    
              // Start file upload
              this.uploadSubscription = this.fileUploadService.uploadFile(file)
                .subscribe({
                  next: event => {
                    // Calculate progress
                    if (event.type === HttpEventType.UploadProgress) {
                      this.uploadProgress = Math.round((100 * event.loaded) / event.total!);
                    } else if (event instanceof HttpResponse) {
                      console.log('Upload complete:', event.body);
                      this.uploading = false;
                    }
                  },
                  error: error => {
                    console.error('Upload error:', error);
                    this.uploading = false;
                  }
               })
          }
    
          onCancelUpload() {
            this.fileUploadService.cancelUpload(); // Trigger cancellation in the service
            this.uploading = false;
            this.uploadProgress = 0;
            if (this.uploadSubscription) {
              this.uploadSubscription.unsubscribe(); 
            }
          }
        }