AzureBlobで差分アップロード

azcopyでは最終更新時間しか確認してくれない問題

featuredimg

# Blobへ差分アップロード

一番簡単な方法は、azcopyを利用する方法で大抵の場合はこれで事足りるでしょう。

azcopy sync ローカルコピー元ディレクトリ コピー先StorageURL --delete-destination true

この場合、既に同期されたファイルはスキップされて新規に更新されたファイルのみ転送してくれるため非常に便利です。

ただし、最終更新日時だけを見てチェックしているためファイル内容がまったく同じでもタイムスタンプが更新されていれば転送されてしまいます。

タイムスタンプに関係なく、md5ハッシュをチェックして内容に変更があったファイルのみ転送するスクリプトを今回作成しました。

# 差分転送コード例

VuePressで静的サイトをビルドするとdistフォルダごと削除されてから再生成されるため、md5を1ファイルずつチェックして差分のあるファイルだけ送信しています。

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const mime = require('mime');

const { BlobServiceClient } = require("@azure/storage-blob");
const account = "<<YOUR_STORAGEACCOUNT_NAME>>";
const sas = "<<STORAGE_SAS>>";
const targetDir = 'docs/.vuepress/dist';

const blobServiceClient = new BlobServiceClient(
  `https://${account}.blob.core.windows.net${sas}`
);

const suffixPath = 'docs/.vuepress/dist/';

function getFiles(dir) {
  const files = fs.readdirSync(dir);
  const resp = [];
  files.forEach(function(file){ 
    const fullPath = path.join(dir, file);
    const stats = fs.statSync(fullPath);
    if(stats.isDirectory()){
      resp.push(...getFiles(fullPath));
    }else{ // ファイルの場合
      resp.push(fullPath.replace(/\\/g, '/'));
    }
  });
  return resp;
}

function md5file(filePath) {
  const target = fs.readFileSync(filePath);
  const md5hash = crypto.createHash('md5');
  md5hash.update(target);
  return md5hash.digest("hex");
}

(async function() {
  const containerClient = blobServiceClient.getContainerClient('$web');
  const blobs = containerClient.listBlobsFlat();

  // サーバ上のファイル一覧を取得
  const uploaded = {};
  for await (const blob of blobs) {
    const md5 = blob.properties.contentMD5.toString('hex');
    uploaded[blob.name] = md5;
  }

  // アップロード予定のファイル一覧を取得
  const locals = {};
  const files = getFiles(targetDir);
  for(const file of files){
    const md5 = md5file(file);
    const contentType= mime.getType(file);
    locals[file.replace(suffixPath, '')] = {
      md5: md5,
      type: contentType
    };
  }

  // upsert対象ファイルを絞り込み
  // 未アップロードもしくはmd5が一致しない場合upsert
  const upserts = Object.keys(locals).filter(p => !uploaded[p] || locals[p].md5 !== uploaded[p]);

  // 削除対象ファイルを絞り込み
  const deletes = Object.keys(uploaded).filter(p => !locals[p]);

  console.log('upsert対象');
  console.log(upserts);
  console.log('');
  console.log('delete対象');
  console.log(deletes);
  console.log('');

  const promises = [];
  // delete blob
  for(const blobName of deletes){
    const blockBlobClient = containerClient.getBlockBlobClient(blobName);
    promises.push(blockBlobClient.delete());
  }

  // upsert blob
  for(const blobName of upserts){
    const file = locals[blobName];
    const blockBlobClient = containerClient.getBlockBlobClient(blobName);
    let cacheControl = 'public, max-age=60'; // デフォルトで60秒有効
    if (blobName.indexOf('assets') >= 0 || blobName.indexOf('favicon') >= 0) {
      // assetsの場合は1年有効
      cacheControl = 'public, max-age=31536000';
    }
    const buffer = Buffer.from(file.md5, 'hex');
    const md5 = new Uint8Array(buffer);
    promises.push(blockBlobClient.uploadFile(`${suffixPath}${blobName}`, {
      blobHTTPHeaders: {
        blobContentType: file.type,
        blobContentMD5: md5,
        blobCacheControl: cacheControl
      }
    }));
  }
  await Promise.all(promises);
  console.log('complete');
}());