/// <summary> /// Handles deleting a specific version of an image file according to file spec. /// </summary> private static async Task DeleteImageFileAsync(Image image, ImageFileSpec imageFileSpec) { if (image == null) { throw new ArgumentNullException(nameof(image)); } var storageId = imageFileSpec.FileSpec switch { FileSpec.SpecOriginal => image.Files.OriginalId, FileSpec.Spec3840 => image.Files.Spec3840Id, FileSpec.Spec2560 => image.Files.Spec2560Id, FileSpec.Spec1920 => image.Files.Spec1920Id, FileSpec.Spec800 => image.Files.Spec800Id, FileSpec.SpecLowRes => image.Files.SpecLowResId, _ => string.Empty }; if (!string.IsNullOrEmpty(storageId)) { var container = Server.Instance.BlobServiceClient.GetBlobContainerClient(imageFileSpec.ContainerName); var response = await container.DeleteBlobIfExistsAsync(storageId, DeleteSnapshotsOption.IncludeSnapshots); Log.Debug("ImageServer.DeleteImageFileAsync: response status: " + response.Value); return; } Log.Debug("ImageServer.DeleteImageFileAsync: storage id is null. FileSpec: " + imageFileSpec.FileSpec); }
/// <summary> /// Causes the metadata on an image to be re-inspected and then any relevant properties on the Image updated. /// </summary> public async Task ReprocessImageMetadataAsync(Image image) { // create the message and send to the Azure Storage queue // message format: {operation}:{image_id}:{gallery_id}:{gallery_category_id}:{overwrite_image_properties} var ids = $"{WorkerOperation.ReprocessMetadata}:{image.Id}:{image.GalleryId}:{image.GalleryCategoryId}:true"; Log.Debug($"ReprocessImageMetadataAsync() - Sending message (pre-base64 encoding): {ids}"); var messageText = Utilities.Base64Encode(ids); await Server.Instance.ImageProcessingQueueClient.SendMessageAsync(messageText); }
/// <summary> /// Attempts to return the image after to a given image in terms of the order they are shown in their gallery. May return null. /// </summary> public async Task <Image> GetNextImageInGalleryAsync(Image currentImage) { // choose the right query - not all galleries will have had their images ordered. fall back to image creation date for unordered galleries var query = currentImage.Position.HasValue ? new QueryDefinition("SELECT TOP 1 VALUE c.id FROM c WHERE c.GalleryId = @galleryId AND c.Position > @position ORDER BY c.Position") .WithParameter("@galleryId", currentImage.GalleryId) .WithParameter("@position", currentImage.Position.Value) : new QueryDefinition("SELECT TOP 1 VALUE c.id FROM c WHERE c.GalleryId = @galleryId AND c.Created > @created ORDER BY c.Created") .WithParameter("@galleryId", currentImage.GalleryId) .WithParameter("@created", currentImage.Created); var id = await GetImageIdByQueryAsync(query); return(await GetImageAsync(currentImage.GalleryId, id)); }
private async Task HandleDeletePreGenImagesAsync(Image image) { await DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec3840)); await DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec2560)); await DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec1920)); await DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec800)); await DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.SpecLowRes)); image.Files.Spec3840Id = null; image.Files.Spec2560Id = null; image.Files.Spec1920Id = null; image.Files.Spec800Id = null; image.Files.SpecLowResId = null; await UpdateImageAsync(image); }
public async Task DeleteCommentAsync(Gallery gallery, Image image, Comment comment) { var removed = image.Comments.Remove(comment); if (removed) { image.CommentCount--; await Server.Instance.Images.UpdateImageAsync(image); gallery.CommentCount--; await Server.Instance.Galleries.UpdateGalleryAsync(gallery); } else { Log.Information($"GalleryServer.DeleteCommentAsync(): No comment removed. imageId={image.Id}, commentCreatedTicks={comment.Created.Ticks}, commentCreatedByUserId={comment.CreatedByUserId}"); } }
/// <summary> /// Updates an Image in the database. Must be provided with a complete and recently queried from the database image to avoid losing other recent updates. /// </summary> public async Task UpdateImageAsync(Image image) { if (image == null) { throw new ArgumentNullException(nameof(image)); } if (!image.IsValid()) { throw new InvalidOperationException("Image is invalid. Please check all required properties are set."); } var container = Server.Instance.Database.GetContainer(Constants.ImagesContainerName); var response = await container.ReplaceItemAsync(image, image.Id, new PartitionKey(image.GalleryId)); Log.Debug($"ImageServer.UpdateImageAsync: Request charge: {response.RequestCharge}. Elapsed time: {response.Diagnostics.GetClientElapsedTime().TotalMilliseconds} ms"); }
/// <summary> /// Deletes the original image and any generated images for an Image. /// </summary> private static async Task DeleteImageFilesAsync(Image image, bool clearImageFileReferences = false) { // delete all image files var deleteTasks = new List <Task> { DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.SpecOriginal)), DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec3840)), DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec2560)), DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec1920)), DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.Spec800)), DeleteImageFileAsync(image, ImageFileSpecs.GetImageFileSpec(FileSpec.SpecLowRes)) }; await Task.WhenAll(deleteTasks); if (clearImageFileReferences) { image.Files.OriginalId = null; image.Files.Spec3840Id = null; image.Files.Spec2560Id = null; image.Files.Spec1920Id = null; image.Files.Spec800Id = null; image.Files.SpecLowResId = null; } }
/// <summary> /// Causes an Image to be permanently deleted from storage and database. /// Will result in some images being re-ordered to avoid holes in positions. /// </summary> /// <param name="image">The Image to be deleted.</param> /// <param name="isGalleryBeingDeleted">Will disable all re-ordering and gallery thumbnail tasks if the gallery is being deleted.</param> public async Task DeleteImageAsync(Image image, bool isGalleryBeingDeleted = false) { await DeleteImageFilesAsync(image); // make note of the image position and gallery id as we might have to re-order photos var position = image.Position; var galleryId = image.GalleryId; // finally, delete the database record var imagesContainer = Server.Instance.Database.GetContainer(Constants.ImagesContainerName); var deleteResponse = await imagesContainer.DeleteItemAsync <Image>(image.Id, new PartitionKey(image.GalleryId)); Log.Debug($"ImageServer:DeleteImageAsync: Request charge: {deleteResponse.RequestCharge}. Elapsed time: {deleteResponse.Diagnostics.GetClientElapsedTime().TotalMilliseconds} ms"); // if necessary, re-order photos down-position from where the deleted photo used to be if (!isGalleryBeingDeleted) { if (position.HasValue) { Log.Debug("ImageServer:DeleteImageAsync: Image had an order, re-ordering subsequent images..."); // get the ids of images that have a position down from where our deleted image used to be var queryDefinition = new QueryDefinition("SELECT c.id AS Id, c.GalleryId AS PartitionKey FROM c WHERE c.GalleryId = @galleryId AND c.Position > @position ORDER BY c.Position") .WithParameter("@galleryId", galleryId) .WithParameter("@position", position.Value); var ids = await Server.GetIdsByQueryAsync(Constants.GalleriesContainerName, queryDefinition); foreach (var databaseId in ids) { var affectedImage = await GetImageAsync(galleryId, databaseId.Id); // this check shouldn't be required as if one image has a position then all should // but life experience suggests it's best to be sure. if (affectedImage.Position == null) { continue; } affectedImage.Position = position; await UpdateImageAsync(affectedImage); position += 1; } } // do we need to update the gallery thumbnail after we delete this image? var gallery = await Server.Instance.Galleries.GetGalleryAsync(image.GalleryCategoryId, image.GalleryId); var newThumbnailNeeded = gallery.ThumbnailFiles == null || gallery.ThumbnailFiles.OriginalId == image.Files.OriginalId; if (newThumbnailNeeded) { var images = await GetGalleryImagesAsync(gallery.Id); if (images.Count > 0) { var orderedImages = Utilities.OrderImages(images); gallery.ThumbnailFiles = orderedImages.First().Files; Log.Debug($"ImageServer.DeleteImageAsync: New gallery thumbnail was needed. Set to {gallery.ThumbnailFiles.Spec800Id}"); } else { gallery.ThumbnailFiles = null; Log.Debug("ImageServer.DeleteImageAsync: New gallery thumbnail was needed but no images to choose from."); } await Server.Instance.Galleries.UpdateGalleryAsync(gallery); } } }
/// <summary> /// Stores an uploaded file in the storage system and adds a supporting Image object to the database. /// </summary> /// <param name="galleryCategoryId">The id for the category the gallery resides in, which the image resides in.</param> /// <param name="galleryId">The gallery this image is going to be contained within.</param> /// <param name="imageStream">The stream for the uploaded image file.</param> /// <param name="filename">The original filename provided by the client.</param> /// <param name="image">Optionally supply a pre-populated Image object.</param> /// <param name="performImageDimensionsCheck">Ordinarily images must be bigger than 800x800 in size but for migration purposes we might want to override this.</param> public async Task CreateImageAsync(string galleryCategoryId, string galleryId, Stream imageStream, string filename, Image image = null, bool performImageDimensionsCheck = true) { try { if (string.IsNullOrEmpty(galleryId)) { throw new ArgumentNullException(nameof(galleryId)); } if (imageStream == null) { throw new ArgumentNullException(nameof(imageStream)); } if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException(nameof(filename)); } await CheckImageDimensionsAsync(imageStream); if (image == null) { // create the Image object anew var id = Utilities.GenerateId(); image = new Image { Id = id, GalleryCategoryId = galleryCategoryId, GalleryId = galleryId, Files = { OriginalId = id + Path.GetExtension(filename).ToLower() } }; } else { // the Image already exists but may not be sufficiently populated... if (!image.Id.HasValue()) { image.Id = Utilities.GenerateId(); } if (!image.GalleryCategoryId.HasValue()) { image.GalleryCategoryId = galleryCategoryId; } if (!image.GalleryId.HasValue()) { image.GalleryId = galleryId; } if (!image.Files.OriginalId.HasValue()) { image.Files.OriginalId = image.Id + Path.GetExtension(filename).ToLower(); } } // this should be done in the worker when parsing the metadata. // we're just not doing it there as that means some work to sanitise and serialise the filename for insertion into the worker message. // lazy, I know. I'll come back to this. image.Metadata.OriginalFilename = filename; if (!image.Name.HasValue()) { image.Name = Utilities.TidyImageName(Path.GetFileNameWithoutExtension(filename)); } if (!image.IsValid()) { throw new InvalidOperationException("Image would be invalid. Please check all required properties are set."); } // upload the original file to storage var originalContainerClient = Server.Instance.BlobServiceClient.GetBlobContainerClient(Constants.StorageOriginalContainerName); await originalContainerClient.UploadBlobAsync(image.Files.OriginalId, imageStream); // create the database record var container = Server.Instance.Database.GetContainer(Constants.ImagesContainerName); var response = await container.CreateItemAsync(image, new PartitionKey(image.GalleryId)); Log.Debug($"ImageServer.CreateImageAsync: Request charge: {response.RequestCharge}. Elapsed time: {response.Diagnostics.GetClientElapsedTime().TotalMilliseconds} ms"); // have the pre-gen images created by a background process await PostProcessImagesAsync(image); } finally { // make sure we release valuable server resources in the event of a problem creating the image imageStream?.Close(); } }