Have UploadedFileManager react client fetch and use upload URLs
diff --git a/frontend/src/app/connection/DefaultStreamlitEndpoints.ts b/frontend/src/app/connection/DefaultStreamlitEndpoints.ts
index 43ea9ee111db..6c3f9b6fcfa6 100644
--- a/frontend/src/app/connection/DefaultStreamlitEndpoints.ts
+++ b/frontend/src/app/connection/DefaultStreamlitEndpoints.ts
@@ -131,10 +131,6 @@ export class DefaultStreamlitEndpoints implements StreamlitEndpoints {
       if (typeof response.data === "number") {
         return response.data
       }
-
-      throw new Error(
-        `Bad uploadFile response: expected a number but got '${response.data}'`
-      )
     })
   }
 
diff --git a/frontend/src/lib/components/widgets/FileUploader/FileUploader.tsx b/frontend/src/lib/components/widgets/FileUploader/FileUploader.tsx
index 533b66a99c88..6891275703b7 100644
--- a/frontend/src/lib/components/widgets/FileUploader/FileUploader.tsx
+++ b/frontend/src/lib/components/widgets/FileUploader/FileUploader.tsx
@@ -22,6 +22,7 @@ import { FileRejection } from "react-dropzone"
 import {
   FileUploader as FileUploaderProto,
   FileUploaderState as FileUploaderStateProto,
+  FileURLsResponse as FileURLsResponseProto,
   UploadedFileInfo as UploadedFileInfoProto,
 } from "src/lib/proto"
 import { FormClearHelper } from "src/lib/components/widgets/Form"
@@ -106,11 +107,21 @@ class FileUploader extends React.PureComponent<Props, State> {
       files: uploadedFileInfo.map(f => {
         const name = f.name as string
         const size = f.size as number
+
+        // TODO(vdonato): Stop using the serverFileId field, and use
+        // the uploadFileUrl/deleteFileUrl/fileId fields instead of fileUrl.
         const serverFileId = f.id as number
+        const fileId = f.fileId as string
+        const deleteFileURL = f.fileDeleteUrl as string
 
         return new UploadFileInfo(name, size, this.nextLocalFileId(), {
           type: "uploaded",
           serverFileId,
+
+          // TODO(vdonato / kajarenc): Use the finalized fields here
+          fileId,
+          uploadFileURL: "TODO",
+          deleteFileURL,
         })
       }),
       newestServerFileId: Number(maxFileId),
@@ -202,25 +213,22 @@ class FileUploader extends React.PureComponent<Props, State> {
    * deleted when the script is re-run.
    */
   private createWidgetValue(): FileUploaderStateProto | undefined {
-    if (this.state.newestServerFileId === 0) {
-      // If newestServerFileId is 0, we've had no transaction with the server,
-      // and therefore no widget value.
-      return undefined
-    }
-
     const uploadedFileInfo: UploadedFileInfoProto[] = this.state.files
       .filter(f => f.status.type === "uploaded")
       .map(f => {
         const { name, size, status } = f
         return new UploadedFileInfoProto({
           id: (status as UploadedStatus).serverFileId,
+          fileId: (status as UploadedStatus).fileId,
+          // TODO(vdonato / kajarenc): Pass the uploadFileURL from
+          // UploadedStatus -> FileUploaderStateProto
+          fileDeleteUrl: (status as UploadedStatus).deleteFileURL,
           name,
           size,
         })
       })
 
     return new FileUploaderStateProto({
-      maxFileId: this.state.newestServerFileId,
       uploadedFileInfo,
     })
   }
@@ -276,12 +284,30 @@ class FileUploader extends React.PureComponent<Props, State> {
       this.removeFile(this.state.files[0].id)
     }
 
-    this.props.uploadClient.fetchFileURLs(acceptedFiles).then((resp: any) => {
-      console.error(resp)
-    })
-
-    // Upload each accepted file.
-    acceptedFiles.forEach(this.uploadFile)
+    this.props.uploadClient
+      .fetchFileURLs(acceptedFiles)
+      .then((fileURLsArray: FileURLsResponseProto.IFileURLs[]) => {
+        _.zip(fileURLsArray, acceptedFiles).forEach(
+          ([fileURLs, acceptedFile]) => {
+            this.uploadFile(
+              fileURLs as FileURLsResponseProto.IFileURLs,
+              acceptedFile as File
+            )
+          }
+        )
+      })
+      .catch((errorMessage: string) => {
+        // TODO(vdonato): Maybe generalize this + the if (rejectedFiles.length > 0) case
+        // below to not duplicate so much code.
+        this.addFiles(
+          acceptedFiles.map(f => {
+            return new UploadFileInfo(f.name, f.size, this.nextLocalFileId(), {
+              type: "error",
+              errorMessage,
+            })
+          })
+        )
+      })
 
     // Create an UploadFileInfo for each of our rejected files, and add them to
     // our state.
@@ -305,7 +331,10 @@ class FileUploader extends React.PureComponent<Props, State> {
     }
   }
 
-  public uploadFile = (file: File): void => {
+  public uploadFile = (
+    fileURLs: FileURLsResponseProto.IFileURLs,
+    file: File
+  ): void => {
     // Create an UploadFileInfo for this file and add it to our state.
     const cancelToken = axios.CancelToken.source()
     const uploadingFileInfo = new UploadFileInfo(
@@ -323,15 +352,12 @@ class FileUploader extends React.PureComponent<Props, State> {
     this.props.uploadClient
       .uploadFile(
         this.props.element,
-        // TODO(vdonato): Use the file upload URL received from the server.
-        "/_stcore/upload_file",
+        fileURLs.uploadUrl as string,
         file,
         e => this.onUploadProgress(e, uploadingFileInfo.id),
         cancelToken.token
       )
-      .then(newFileId =>
-        this.onUploadComplete(uploadingFileInfo.id, newFileId)
-      )
+      .then(newFileId => this.onUploadComplete(uploadingFileInfo.id, fileURLs))
       .catch(err => {
         // If this was a cancel error, we don't show the user an error -
         // the cancellation was in response to an action they took.
@@ -353,14 +379,8 @@ class FileUploader extends React.PureComponent<Props, State> {
    */
   private onUploadComplete = (
     localFileId: number,
-    serverFileId: number
+    fileURLs: FileURLsResponseProto.IFileURLs
   ): void => {
-    // "state.newestServerFileId" must always hold the max fileID
-    // returned from the server.
-    this.setState(state => ({
-      newestServerFileId: Math.max(state.newestServerFileId, serverFileId),
-    }))
-
     const curFile = this.getFile(localFileId)
     if (curFile == null || curFile.status.type !== "uploading") {
       // The file may have been canceled right before the upload
@@ -370,7 +390,13 @@ class FileUploader extends React.PureComponent<Props, State> {
 
     this.updateFile(
       curFile.id,
-      curFile.setStatus({ type: "uploaded", serverFileId })
+      curFile.setStatus({
+        type: "uploaded",
+        serverFileId: 0,
+        fileId: fileURLs.fileId as string,
+        uploadFileURL: fileURLs.uploadUrl as string,
+        deleteFileURL: fileURLs.deleteUrl as string,
+      })
     )
   }
 
@@ -416,6 +442,10 @@ class FileUploader extends React.PureComponent<Props, State> {
       file.status.cancelToken.cancel()
     }
 
+    if (file.status.type === "uploaded" && file.status.deleteFileURL) {
+      this.props.uploadClient.deleteFile(file.status.deleteFileURL)
+    }
+
     this.removeFile(fileId)
   }
 
diff --git a/frontend/src/lib/components/widgets/FileUploader/UploadFileInfo.ts b/frontend/src/lib/components/widgets/FileUploader/UploadFileInfo.ts
index 4e382136577c..93669b271ec0 100644
--- a/frontend/src/lib/components/widgets/FileUploader/UploadFileInfo.ts
+++ b/frontend/src/lib/components/widgets/FileUploader/UploadFileInfo.ts
@@ -27,6 +27,12 @@ export interface UploadedStatus {
 
   /** ID generated by the server. */
   serverFileId: number
+
+  fileId: string
+  // TODO(vdonato / kajarenc): Use the same names here as in the
+  // UploadedFileInfo proto type.
+  uploadFileURL: string
+  deleteFileURL?: string
 }
 
 export interface ErrorStatus {
diff --git a/lib/streamlit/elements/file_uploader.py b/lib/streamlit/elements/file_uploader.py
index a85006bc9f5b..1d0df0f4d085 100644
--- a/lib/streamlit/elements/file_uploader.py
+++ b/lib/streamlit/elements/file_uploader.py
@@ -114,7 +114,10 @@ def serialize(self, files: SomeUploadedFiles) -> FileUploaderStateProto:
             file_info.file_id = f.file_id
             file_info.name = f.name
             file_info.size = f.size
-            file_info.file_delete_url = self.file_delete_urls[f.file_id]
+            try:
+                file_info.file_delete_url = self.file_delete_urls[f.file_id]
+            except:
+                pass
 
         return state_proto
 
diff --git a/proto/streamlit/proto/Common.proto b/proto/streamlit/proto/Common.proto
index 5c6b590c5a5d..071d65f0f021 100644
--- a/proto/streamlit/proto/Common.proto
+++ b/proto/streamlit/proto/Common.proto
@@ -42,10 +42,10 @@ message UInt32Array {
   repeated uint32 data = 1;
 }
 
-// TODO(vdonato): Finalize the next two proto types. We currently have enough
-// information here to support pure OS use cases, but we'll need to coordinate
-// with the SiS team to verify that we're passing enough file metadata back for
-// them.
+// TODO(vdonato / kajarenc): Finalize the next two proto types. We currently
+// have enough information here to support pure OS use cases, but we'll need to
+// coordinate with the SiS team to verify that we're passing enough file
+// metadata back for them.
 message FileURLsRequest {
   string request_id = 1;
   repeated string file_names = 2;
@@ -54,6 +54,8 @@ message FileURLsRequest {
 
 message FileURLsResponse {
   // TODO(vdonato): Maybe come up with a better name for this proto type.
+  // TODO(vdonato / kajarenc): Maybe move this outside of FileURLsResponse so that
+  //                we can use this type in UploadedFileInfo.
   message FileURLs {
     string file_id = 1;
     string upload_url = 2;
@@ -75,10 +77,13 @@ message UploadedFileInfo {
   // The size of this file in bytes.
   uint32 size = 3;
 
-  // File id that can be used to retrieve a file.
+  // ID that can be used to retrieve a file.
   string file_id = 4;
 
-  // File url to delete the file
+  // TODO(vdonato / kajarenc): Add a file_upload_url field. Or use the factored
+  // out FileURLs type from the above TODO.
+
+  // URL to delete the file
   string file_delete_url = 5;
 }
 
