基於.NetCore開發部落格專案 StarBlog - (25) 圖片介面與檔案上傳

程式設計實驗室發表於2022-12-22

前言

上傳檔案的介面設計有兩種風格,一種是整個專案只設定一個介面用來上傳,然後其他需要用到檔案的地方,都只存一個引用ID;另一種是每個需要檔案的地方單獨管理各自的檔案。這倆各有優劣吧,本專案中選擇的是後者的風格,文章圖片和照片模組又要能CRUD又要批次匯入,還是各自管理檔案比較好。

圖片介面

說會正題,先介紹一下圖片相關介面。

圖片列表

首先CRUD是肯定有的,圖片列表的分頁檢視也是有的,不過因為篩選功能沒有做,所以就不定義一個ViewModel作為引數了。

控制器程式碼 StarBlog.Web/Apis/Blog/PhotoController.cs

[HttpGet]
public ApiResponsePaged<Photo> GetList(int page = 1, int pageSize = 10) {
    var paged = _photoService.GetPagedList(page, pageSize);
    return new ApiResponsePaged<Photo> {
        Pagination = paged.ToPaginationMetadata(),
        Data = paged.ToList()
    };
}

跟部落格前臺公用一套圖片列表邏輯,所以這部分抽出來放在service,程式碼如下

StarBlog.Web/Services/PhotoService.cs

public IPagedList<Photo> GetPagedList(int page = 1, int pageSize = 10) {
    return _photoRepo.Select.OrderByDescending(a => a.CreateTime)
        .ToList().ToPagedList(page, pageSize);
}

單個圖片

獲取單個圖片,跟獲取文章的差不多,傳入ID,找不到就返回404,找到就返回圖片物件

[HttpGet("{id}")]
public ApiResponse<Photo> Get(string id) {
    var photo = _photoService.GetById(id);
    return photo == null
        ? ApiResponse.NotFound($"圖片 {id} 不存在")
        : new ApiResponse<Photo> {Data = photo};
}

圖片縮圖

在本系列第20篇中,本專案已經實現了圖片顯示的最佳化,詳見:基於.NetCore開發部落格專案 StarBlog - (20) 圖片顯示最佳化

除了 ImageSharp 元件提供的圖片縮圖功能外,我這裡還寫了另一個生成縮圖的方法,這個方法有倆特點

  • 直接在記憶體中生成返回,不會寫入快取檔案
  • 生成的是Progressive JPEG格式,目前 ImageSharp 是不支援的,可以最佳化前端的載入速度

控制器程式碼

[HttpGet("{id}/Thumb")]
public async Task<IActionResult> GetThumb(string id, [FromQuery] int width = 300) {
    var data = await _photoService.GetThumb(id, width);
    return new FileContentResult(data, "image/jpeg");
}

service程式碼

/// <summary>
/// 生成Progressive JPEG縮圖 (使用 MagickImage)
/// </summary>
/// <param name="width">設定為0則不調整大小</param>
public async Task<byte[]> GetThumb(string id, int width = 0) {
    var photo = await _photoRepo.Where(a => a.Id == id).FirstAsync();
    using (var image = new MagickImage(GetPhotoFilePath(photo))) {
        image.Format = MagickFormat.Pjpeg;
        if (width != 0) {
            image.Resize(width, 0);
        }

        return image.ToByteArray();
    }
}

這個 MagickImage 是用 C++ 寫的,在不同平臺上引用不同 native 庫,需要在 csproj 裡面寫上配置,這樣釋出的時候才會帶上對應的依賴庫,而且似乎在 CentOS 系統上會有坑…

<!--  複製 Magick 庫  -->
<PropertyGroup>
    <MagickCopyNativeWindows>true</MagickCopyNativeWindows>
    <MagickCopyNativeLinux>true</MagickCopyNativeLinux>
    <MagickCopyNativeMacOS>true</MagickCopyNativeMacOS>
</PropertyGroup>

其他介面

還有一些介面,跟之前介紹的大同小異,再重複一次也意義不大,讀者有需要的話可以自行檢視原始碼。

圖片檔案上傳

這個同時也是圖片的新增介面

先定義DTO

public class PhotoCreationDto {
    /// <summary>
    /// 作品標題
    /// </summary>
    [Required(ErrorMessage = "作品標題不能為空")]
    public string Title { get; set; }

    /// <summary>
    /// 拍攝地點
    /// </summary>
    [Required(ErrorMessage = "拍攝地點不能為空")]
    public string Location { get; set; }
}

控制器程式碼

[Authorize]
[HttpPost]
public ApiResponse<Photo> Add([FromForm] PhotoCreationDto dto, IFormFile file) {
    var photo = _photoService.Add(dto, file);

    return !ModelState.IsValid
        ? ApiResponse.BadRequest(ModelState)
        : new ApiResponse<Photo>(photo);
}

因為上傳的同時還要附帶一些資料,需要使用 FormData 傳參,所以這裡使用 [FromForm] 特性標記這個 dto 引數

IFormFile 型別的引數可以拿到上傳上來的檔案

下面是service程式碼

public Photo Add(PhotoCreationDto dto, IFormFile photoFile) {
    var photoId = GuidUtils.GuidTo16String();
    var photo = new Photo {
        Id = photoId,
        Title = dto.Title,
        CreateTime = DateTime.Now,
        Location = dto.Location,
        FilePath = Path.Combine("photography", $"{photoId}.jpg")
    };

    var savePath = Path.Combine(_environment.WebRootPath, "media", photo.FilePath);
	
    // 如果超出最大允許的大小,則按比例縮小
    const int maxWidth = 2000;
    const int maxHeight = 2000;
    using (var image = Image.Load(photoFile.OpenReadStream())) {
        if (image.Width > maxWidth)
            image.Mutate(a => a.Resize(maxWidth, 0));
        if (image.Height > maxHeight)
            image.Mutate(a => a.Resize(0, maxHeight));
        image.Save(savePath);
    }

    // 儲存檔案
    using (var fs = new FileStream(savePath, FileMode.Create)) {
        photoFile.CopyTo(fs);
    }

    // 讀取圖片的尺寸等資料
    photo = BuildPhotoData(photo);

    return _photoRepo.Insert(photo);
}

這裡對圖片做了一些處理,拋開這些細節,其實對上傳的檔案,最關鍵的只有幾行儲存程式碼

using (var fs = new FileStream("savePath", FileMode.Create)) {
    photoFile.CopyTo(fs);
}

這樣就完成了檔案上傳介面。

系列文章

相關文章