前言

最近刚刚把图床迁移到 Azure, 因为 github 的图片不太好用,国内经常看不见

然而吧, 那是正则批量扫描 markdown 文件, 然后下载文件, 直接使用 git 管理的, 几百张的时候倒是还可以接受

但今后如果单张图片也需要这么做, 就很麻烦了, 以前是用 picGo 上传的图片

虽然现在 picGO 支持自己写插件, 但是 js 不是我的强项, 所以想自己写一个试试看

Api 分析

azure 有 Rest api 可以完成上传的步骤, 文档

然而这个 api 需要一个 commitId, 不像 github 的 api 比较智能, 直接上传就行.

但没关系, 我们可以通过两次接口访问得到它.

使用 postman 来测试下

访问 https://dev.azure.com/{{organization}}/{{project}}/_apis/git/repositories/{{repositoryId}}/pushes?api-version=5.0 得到一个结果

img

然后访问这个 url 参数的接口可以获取 commitId

img

也就是 git 的 commit hash 值

img

这里把 oldObjectId 替换成刚刚拿到的 commitId, 其他的根据自己的情况替换, 比如 comment 是注释, path 是你要放到哪个目录和名字, content 是图片的 base64 值

api 流程通了, 接下来就是技术的选择了

技术选型

因为 dart 没有合适的 api 可以调用到剪切板, 所以我使用 swift 来完成这一步, 然后直接 base64 后输出到控制台

接着使用 dart 进行”接力”, 把图片的 base64 格式的图片上传到 azure 即可

至于为啥这么做, 我只能说, swift 不支持 async await 语法, 然后常用的 http 库都不支持同步访问, 我需要链式调用 3 个 api 真的是太麻烦了

撸码

swift 部分

创建一个 swift 的 command line 工程(macOS 版)

main.swift:

//
//  main.swift
//  ReadClipboard
//
//  Created by cjl on 2020/1/30.
//  Copyright © 2020 cjl. All rights reserved.
//

import Foundation
import AppKit

class UploadManager{

    let pasteboard = NSPasteboard.general

    func uploadFirst(){
        if let data = pasteboard.data(forType: .fileURL){
            // 上传文件类型的
            guard let fileUri = NSString(data: data , encoding: String.Encoding.utf8.rawValue) else{
                print("虽然是文件类型, 但解析失败 : \(data)");
                return
            }
            guard let url = URL(string: fileUri as String) else{
                print("转为 URL 失败")
                return
            }
            print("文件类型: \(url)")
            guard let contentData = try? Data(contentsOf: url) else{
                print("读取文件失败")
                return
            }
            print("content count: \(contentData.count)")
//            let last = url.lastPathComponent
            upload(data: contentData)
            return
        }

        // 图片类型
        if let data = pasteboard.data(forType: .png){
            print("图片类型, 长度: \(data.count)")
            upload(data: data)
            return
        }

        // 文件内容类型
        if let data = pasteboard.data(forType: .fileContents){
            print("文件内容类型")
            upload(data: data)
            return
        }

        print("其他类型 : \(String(describing: pasteboard.pasteboardItems?.first?.types))")

        exit(-1)
    }

    func upload(data:Data){
        let b64s = data.base64EncodedString()
        print("base64:\(b64s)")
    }
}

let manager = UploadManager()
manager.uploadFirst()

这里就是获取剪切板, 只导出文件或图片类型, 然后转为 base64, 接着拼接 base64: 然后输出下

之所以拼接是因为这样可以在 dart 端根据开头字符串来过滤其他日志信息,以便于”接力”

dart 部分

核心代码如下:

import 'dart:convert';
import 'dart:io';

import 'package:http/io_client.dart';
import 'package:upload_image/src/uploader.dart';
import 'package:http/http.dart' as http;

class AzureUploader with Uploader {
  String org;
  String project;
  String repo;
  String username;
  String password;

  String get url =>
      'https://dev.azure.com/$org/$project/_apis/git/repositories/$repo/pushes?api-version=5.0';

  @override
  void initConfig(Map<String, dynamic> configs) {
    org = configs['org'];
    project = configs['project'];
    repo = configs['repo'];
    username = configs['user'];
    password = configs['token'];
  }

  @override
  Future<String> upload(String imageContent) async {
    final lastCommitId = await _getLastCommitId();
    print('lastCommitId = $lastCommitId');
    return _upload(imageContent, lastCommitId);
  }

  Future<String> _getLastCommitId() async {
    final response = await http.get(url);
    final map = json.decode(response.body);
    final commitResponse = await http.get(map['value'][0]['url']);
    return json.decode(commitResponse.body)['commits'][0]['commitId'];
  }

  Future<String> _upload(String imageContent, String lastCommitId) async {
    final token = base64.encode(ascii.encode('$username:$password'));
    final now = DateTime.now();
    final dt = now.toLocal().toString();
    final ms = now.millisecondsSinceEpoch;

    final pathName = '$ms.png';

    final body = {
      'refUpdates': [
        {
          'name': 'refs/heads/master',
          'oldObjectId': lastCommitId,
        },
      ],
      'commits': [
        {
          'comment': 'add image at $dt',
          'changes': [
            {
              'changeType': 'add',
              'item': {
                'path': pathName,
              },
              'newContent': {
                'content': imageContent,
                'contentType': 'base64Encoded',
              }
            }
          ],
        }
      ],
    };

    final httpClient = HttpClient();
    // httpClient.findProxy = (proxy) {
    //   return 'PROXY localhost:8888';
    // };

    final client = IOClient(httpClient);

    final response = await client.post(
      url,
      headers: {
        'Authorization': 'Basic $token',
        'Content-Type': 'application/json',
      },
      body: json.encode(body),
    );

    final commitResult = json.decode(response.body);

    final repoUrl = commitResult['repository']['url'];

    final itemUrl =
        '$repoUrl/items?path=%2F${pathName}&versionDescriptor%5BversionOptions%5D=0&versionDescriptor%5BversionType%5D=0&versionDescriptor%5Bversion%5D=master&resolveLfs=true&%24format=octetStream&api-version=5.0&download=true';

    return itemUrl;
  }
}

简单来说, 就是从 config 中读取出用户名,密码(token),工程名,仓库名几个属性, 然后完成 api 的调用, 接着使用 azure 图片的拼接方法来完成图片的上传

最后解析下, 获取到仓库的 url, 拼接 pathName 就可以获取到文件了


最后一步, 使用一个三方库, 把完成的图片 url 自动复制到剪切板

后记

本篇的图片就是用这个方法弄的, 不过还是挺麻烦的, 所以后面我还是会做一个 picGo 的插件来完成这些步骤

更新: picgo 插件开发完成