private string GenerateUploadVerifyToken(LFSRequest.LFSObject obj) { var token = new UploadVerifyToken() { Oid = obj.Oid, Size = obj.Size }; var value = JsonSerializer.Serialize(token); return(dataProtector.Protect(value, UploadTokenValidTime)); }
public async Task <ActionResult <UploadFileResponse> > StartFileUpload( [Required][FromBody] UploadFileRequestForm request) { if (!CheckNewItemName(request.Name, out var badRequest)) { return(badRequest !); } if (!remoteStorage.Configured) { throw new HttpResponseException() { Status = StatusCodes.Status500InternalServerError, Value = "Remote storage is not configured on the server", }; } // Disallow extensions with uppercase letters if (PathParser.IsExtensionUppercase(request.Name)) { return(BadRequest("File extension can't contain uppercase characters")); } // TODO: maybe in the future we'll want to allow anonymous uploads to certain folders var user = HttpContext.AuthenticatedUserOrThrow(); // Check write access StorageItem?parentFolder = null; if (request.ParentFolder != null) { parentFolder = await database.StorageItems.FirstOrDefaultAsync(i => i.Ftype == FileType.Folder && i.Id == request.ParentFolder.Value); if (parentFolder == null) { return(NotFound("Parent folder doesn't exist")); } } // Check if the item already exists (a new version is being uploaded) var parentId = parentFolder?.Id; var existingItem = await database.StorageItems.FirstOrDefaultAsync(i => i.ParentId == parentId && i.Name == request.Name); if (existingItem != null) { // New version of an existing item. User needs at least read access to the folder and // Root folder is publicly readable so that doesn't need to be checked here if (parentFolder != null) { if (!parentFolder.IsReadableBy(user)) { return(this.WorkingForbid("You don't have read access to the folder")); } } // Disallow file uploads to a folder item if (existingItem.Ftype != FileType.File) { return(BadRequest("Can't upload a new file version to an item that is not a file")); } } else { // Write access required to make a new item if (parentFolder == null) { if (!user.HasAccessLevel(UserAccessLevel.Admin)) { return(this.WorkingForbid("Only admins can write to root folder")); } } else { if (!parentFolder.IsWritableBy(user)) { return(this.WorkingForbid("You don't have write access to the folder")); } } } if (existingItem == null) { existingItem = new StorageItem() { Name = request.Name, Ftype = FileType.File, ReadAccess = request.ReadAccess, WriteAccess = request.WriteAccess, AllowParentless = parentId == null, Parent = parentFolder, OwnerId = user.Id, }; await database.StorageItems.AddAsync(existingItem); } var version = await existingItem.CreateNextVersion(database); var file = await version.CreateStorageFile(database, DateTime.UtcNow + AppInfo.RemoteStorageUploadExpireTime, request.Size); string?uploadUrl = null; MultipartFileUpload?multipart = null; long? multipartId = null; string?uploadId = null; if (request.Size >= AppInfo.FileSizeBeforeMultipartUpload) { // Multipart upload is recommended for large files, as large files are hard to make go through // in a reasonable time with a single PUT request try { uploadId = await remoteStorage.CreateMultipartUpload(file.UploadPath, request.MimeType); if (uploadId == null) { throw new Exception("returned uploadId is null"); } } catch (Exception e) { logger.LogError("Failed to create multipart upload: {@E}", e); return(Problem("Failed to create a new multipart upload")); } var chunks = ComputeChunksForFile(request.Size).ToList(); var initialChunksToUpload = AddUploadUrlsToChunks(chunks.Take(AppInfo.MultipartSimultaneousUploads * AppInfo.MultipartUploadPartsToReturnInSingleCall), file.UploadPath, uploadId, AppInfo.RemoteStorageUploadExpireTime).ToList(); var multipartModel = new InProgressMultipartUpload() { UploadId = uploadId, Path = file.UploadPath, NextChunkIndex = initialChunksToUpload.Count, }; await database.InProgressMultipartUploads.AddAsync(multipartModel); await database.SaveChangesAsync(); multipartId = multipartModel.Id; var chunkToken = new ChunkRetrieveToken(multipartModel.Id, file.Id, uploadId); var chunkTokenStr = JsonSerializer.Serialize(chunkToken); multipart = new MultipartFileUpload() { ChunkRetrieveToken = chunkDataProtector.Protect(chunkTokenStr, AppInfo.MultipartUploadTotalAllowedTime), TotalChunks = chunks.Count, NextChunks = initialChunksToUpload, }; } else { // Normal upload (in a single PUT request) await database.SaveChangesAsync(); uploadUrl = remoteStorage.CreatePresignedUploadURL(file.UploadPath, AppInfo.RemoteStorageUploadExpireTime); } // Need to queue a job to calculate the parent folder size if (parentId != null) { jobClient.Enqueue <CountFolderItemsJob>((x) => x.Execute(parentId.Value, CancellationToken.None)); } if (uploadId != null) { jobClient.Schedule <DeleteNonFinishedMultipartUploadJob>((x) => x.Execute(uploadId, CancellationToken.None), AppInfo.MultipartUploadTotalAllowedTime * 2); } // TODO: queue a job to delete the version / UploadPath after a few hours if the upload fails var token = new UploadVerifyToken() { TargetStorageItem = existingItem.Id, TargetStorageItemVersion = version.Id, MultipartId = multipartId, }; var tokenStr = JsonSerializer.Serialize(token); return(new UploadFileResponse() { UploadURL = uploadUrl, Multipart = multipart, TargetStorageItem = existingItem.Id, TargetStorageItemVersion = version.Id, UploadVerifyToken = dataProtector.Protect(tokenStr, multipart == null ? AppInfo.RemoteStorageUploadExpireTime : AppInfo.MultipartUploadTotalAllowedTime), }); }