Box: ファイルアップロード (Box: Upload File)
Box の指定フォルダにファイルをアップロードします。
2020-10-06 (C) Questetra, Inc. (MIT License)
Configs
  • C1: OAuth2 設定名 *
  • C2: アップロードするファイルが保存されているファイル型データ項目 *
  • C3: ファイルをアップロードするフォルダの ID (指定がない場合は、ルートフォルダ) #{EL}
  • C4: ファイル ID を保存する文字型データ項目
  • C5: ファイル URL を保存する文字型データ項目
Script
// OAuth2 config
// Authorization Endpoint URL: https://app.box.com/api/oauth2/authorize
// Token Endpoint URL: https://app.box.com/api/oauth2/token
// Scope:
// Consumer Key: (Get by box Application on box Developers)
// Consumer Secret: (Get by box Application on box Developers)

const SIZE_LIMIT = 52428800; //File size border of Box
//50MB

main();
function main(){
  //// == 工程コンフィグの参照 / Config Retrieving ==
  const oauth2 = configs.get("conf_OAuth2");
  const idDataDef = configs.getObject("fileId");
  const urlDataDef = configs.getObject("fileUrl");
  //// == ワークフローデータの参照 / Data Retrieving ==
  const token = httpClient.getOAuth2Token( oauth2 );
  const folderId = decideFolderId();
  const files = engine.findData(configs.getObject("uploadedFile")  );
  if (files === null) {
    setData(idDataDef,[""]);
    setData(urlDataDef,[""]);
    return;
  } 
  
  //// == 演算 / Calculating ==

  checkFile(files,idDataDef,urlDataDef);
  let uploadedFileData = [[],[]]; //0:ID,1:URL
  let requestNum = 0;
  const session = prepareSessionsAndCheckRequestNumber(token,files,folderId);
  uploadAllFiles(token,files,folderId,uploadedFileData,session);

  //// == ワークフローデータへの代入 / Data Updating ==
  setData(idDataDef,uploadedFileData[0]);
  setData(urlDataDef,uploadedFileData[1]);
}

/**
  * フォルダのIDをconfigから読み出して出力する。空の場合は"0"とする。
  * @return {String}  フォルダのID
  */
function decideFolderId(){
  let folderId = configs.get("uploadedFolderId");
  if(folderId === "" || folderId === null){
    //when folder ID isn't set, set "0"
    folderId = "0";
  }
  return folderId;
}

/**
  * アップロードしようとするファイルの数・名前が適切かどうかチェックする
  * ファイル数が通信制限を超えたらエラーを出し、その後各関数を呼び出す
  * @param {String} token  OAuth2 トークン
  * @param {Array<File>} files アップロードしようとするファイルの配列
  * @param {String} folderId  アップロード先フォルダのID
  * @param {ProcessDataDefinitionView} idDataDef  ID を保存するデータ項目の ProcessDataDefinitionView
  * @param {ProcessDataDefinitionView} urlDataDef  URL を保存するデータ項目の ProcessDataDefinitionView
  */
function checkFile(files,idDataDef,urlDataDef){
  const fileNum = files.size(); //number of files
  if(fileNum > httpClient.getRequestingLimit()){
    throw "Number of requests is over the limit";
  }
  checkfileNum(idDataDef,fileNum);
  checkfileNum(urlDataDef,fileNum);
  checkFileName(files);
  checkFileNameOverlap(files);
}

/**
  * アップロードするファイルが複数で パス・URL 出力先のデータ項目が単一行ならエラーにする
  * @param {ProcessDataDefinitionView} dataDef  データ項目の ProcessDataDefinitionView
  * @param {Number} fileNum  アップロードしようとするファイルの個数
  */
function checkfileNum(dataDef,fileNum){
  if(dataDef !==  null){
    //Multiple Judge
    if(dataDef.matchDataType("STRING_TEXTFIELD") && fileNum > 1){
      throw "Multiple files are set though the Data Item to save the output is Single-line String."
    }
  }
}
/**
  * アップロードするファイルの名前がBoxにおいて無効なファイル名となるものならエラーにする
  * Boxにおいて無効なファイル名となる条件
  * ・ファイル名に\ / のいずれかが含まれている
  * ・ファイル名の先頭か末尾にスペースがある
  * ・ファイル名が. または .. である
  * @param {Array<File>} files  アップロードしようとするファイルの配列
  */
function checkFileName(files){
  for(let i = 0; i < files.size(); i++){
    let fileName = files[i].getName();
    if(fileName.indexOf("/") !== -1 || fileName.indexOf("\\") !== -1 || fileName === "." || fileName === ".." || fileName.endsWith(" ")  === true || fileName.startsWith(" ")  === true){
      throw "Invalid File Name";
    }
  }
}

/**
  * アップロードするファイルの中に同じファイル名のものが2つ以上あればエラー
  * @param {Array<File>}  アップロードしようとするファイルの配列
  */
function checkFileNameOverlap(files){
  let fileNames = [];
  const fileNum = files.size();
  for (let i = 0; i < fileNum; i++){
    if(fileNames.includes(files[i].getName())){
      throw "Two or more files to upload have the same name.";
    }
    fileNames[i] = files[i].getName();
  }
}

/**
  * 50MBを超えるファイルについてセッションを作成し、通信数を確認する
  * 通信数が制限を超えたらエラーを出す
  * 通信数=50MB以下のファイルの数 + <50MBを超えるファイルそれぞれについて>(ceil(ファイルサイズ/パケットサイズ) + 2)
  * パケットサイズはセッション作成時に指定される
  * @param {String} token  OAuth2 トークン
  * @param {Array<File>} files  アップロードしようとするファイルの配列
  * @param {String} folderId  アップロード先フォルダのID
  * @return {Array<Object>} アップロードセッションの配列(50MB以下のファイルについては空オブジェクト)
  */
function prepareSessionsAndCheckRequestNumber(token,files,folderId){
  let requestNum = 0;
  let session = [];
  for(let i = 0; i< files.size(); i++){
    requestNum++;
    if(files.get(i).getLength() > SIZE_LIMIT){
      //over 50MB
      session[i] = createSession(token,files.get(i),folderId);
      requestNum += (Math.ceil(files.get(i).getLength() / session[i].part_size + 1));
    }else{
      session[i] = {};
    }
  }
  if(requestNum > httpClient.getRequestingLimit()){
    throw "Number of requests is over the limit";
  }
  return session;
}

/**
  * 各ファイルのアップロード処理を行う
  * ファイルのサイズごとに工程は異なる
  * @param {String} token  OAuth2 のトークン
  * @param {Array<File>} files  アップロードしようとするファイルの配列
  * @param {String} folderId  アップロード先のフォルダのパス
  * @param {Array<Array<String>>} uploadedFileData  アップロードしたファイルの情報を格納する二次元配列
  * @param {Array<Object>} session アップロードセッションの配列(50MB以下のファイルについては空オブジェクト)
  */
function uploadAllFiles(token,files,folderId,uploadedFileData,session){
  for(let i = 0; i< files.size(); i++){
    engine.log(files.get(i).getName());
    if(files.get(i).getLength() > SIZE_LIMIT){
      //over 50MB
      processLargeFile(token,files.get(i),folderId,uploadedFileData,session[i]);
    }else{
      //under 50MB
      upload(token,files.get(i),folderId,uploadedFileData);
    }
  }
}

/**
  * 50MB以下のファイルをアップロードする。一回につき一つのみ。
  * @param {String} token  OAuth2 トークン
  * @param {File} file  アップロードするファイル
  * @param {String} folderId  アップロード先フォルダのID
  * @param {Array<Array<String>>} uploadedFileData  アップロードしたファイルの情報を格納する二次元配列
  */
function upload(token,file,folderId,uploadedFileData){
  const url = "https://upload.box.com/api/2.0/files/content";
  let attributes = {
      parent : {id : folderId}
    };
  attributes["name"] = String(file.getName());

  let response = httpClient.begin()
        .bearer(token)
        .multipart('attributes', JSON.stringify(attributes))
        .multipart('file', file)
        .post(url);
  //when error thrown
  const responseStr = response.getResponseAsString();
  const status = response.getStatusCode();
  if (status >= 300) {
    engine.log(` upload failure\n${responseStr}`);
    throw `failed to upload\nstatus: ${status}\nfile: ${file.getName()}`;
  }
  engine.log(" upload successful");
  addOutputToArray(responseStr,uploadedFileData);
}

/**
  * 50MBを超えるファイルのアップロード処理を行う
  * @param {String} token  OAuth2 のトークン
  * @param {File} file  アップロードするファイル
  * @param {String} folderId  アップロード先のフォルダのパス
  * @param {Array<Array<String>>} uploadedFileData  アップロードしたファイルの情報を格納する二次元配列
  * @param {Object} session  アップロードセッション
  */
function processLargeFile(token,file,folderId,uploadedFileData,session){
  const sessionId = session.id;
  const fileSize = file.getLength();
  let part;
  let partObj = {"parts" : []};
  let range = 0;
  fileRepository.readFile(file, session.part_size, function(packet){
    //upload each fragment of file
    part = uploadLargeFile(token,sessionId,range,packet,fileSize,uploadedFileData,file.getName());
    partObj.parts.push(part);
    range += part.size;
  });
  commitSession(token,sessionId,partObj,file,uploadedFileData);
}

/**
  * アップロード用のセッションを作成する
  * @param {String} token  OAuth2 のトークン
  * @param {File} file  アップロードするファイル
  * @param {String} folderId  アップロード先のフォルダのID
  * @return {JSON Object}  アップロードセッション
  */
function createSession(token,file,folderId){
  const url = "https://upload.box.com/api/2.0/files/upload_sessions";
  const body = {
    "file_name" : file.getName(),
    "file_size" : file.getLength(),
    "folder_id" : folderId
  };
  let response = httpClient.begin()
    .bearer(token)
    .body(JSON.stringify(body), "application/json; charset=UTF-8")
    .post(url);

  const status = response.getStatusCode();
  const jsonStr = response.getResponseAsString();
  if(status >= 300){
    engine.log(file.getName())
    engine.log(` upload failure\n${jsonStr}`);
    throw `failed to create upload session \nstatus: ${status}\nfile: ${file.getName()}`;
  }
  return JSON.parse(jsonStr);
}

/**
  * 50MBを超えるファイルについて、各部分のアップロードを実行する
  * @param {String} token  OAuth2 のトークン
  * @param {String} sessionId  アップロードセッションの ID
  * @param {Number} range  range
  * @param {ByteArrayWrapper} packet  アップロードするバイナリ
  * @param {Number} fileSize  アップロードするファイルのサイズ
  * @param {Array<Array<String>>} uploadedFileData  アップロードしたファイルの情報を格納する二次元配列
  * @return {Object}  Uploaded Part
  */
function uploadLargeFile(token,sessionId,range,packet,fileSize,uploadedFileData,fileName){
  const packetSize = packet.getLength();
  const url = `https://upload.box.com/api/2.0/files/upload_sessions/${sessionId}`;
  const rangetxt = `bytes ${range}-${range + packetSize - 1}/${fileSize}`;
  const hash = `sha=${base64.encodeToString(digest.sha1(packet))}`;
  let sending = httpClient.begin()
    .bearer(token)
    .header("Content-Range", rangetxt )
    .header("Digest", hash ) 
    .body(packet,"application/octet-stream")
    .put(url);
    
  const status = sending.getStatusCode();
  const responseStr = sending.getResponseAsString();
  const json = JSON.parse(responseStr);
  if(status >= 300){
    engine.log(` upload failure\n${responseStr}`);
    throw `failed to upload\nstatus: ${status}\nfile: ${fileName}`;
  }else{
    return json.part;
  }
}

/**
  * 50MBを超えるファイルについて、アップロードをコミットする
  * @param {String} token  OAuth2 のトークン
  * @param {String} sessionId  アップロードセッションの ID
  * @param {Object} partObj  これまでのアップロートパートをまとめたオブジェクト
  * @param {File} file  アップロードするファイル
  * @param {Array<Array<String>>} uploadedFileData  アップロードしたファイルの情報を格納する二次元配列
  */
function commitSession(token,sessionId,partObj,file,uploadedFileData){
  const url = `https://upload.box.com/api/2.0/files/upload_sessions/${sessionId}/commit`;
  const hash = `sha=${base64.encodeToString(digest.sha1(file))}`;
  let commit = httpClient.begin()
    .bearer(token)
    .header("Digest", hash )
    .body(JSON.stringify(partObj),"application/json")
    .post(url);

  const status = commit.getStatusCode();
  const responseStr = commit.getResponseAsString();
  if(status >= 300){
    engine.log(` upload failure\n${responseStr}`);
    throw `failed to commit upload session \nstatus: ${status}\nfile: ${file.getName()}`;
  }
  engine.log(" upload successful");
  addOutputToArray(responseStr,uploadedFileData);
}
/**
  * アップロードしたデータのIDとURLを配列にセットする。
  * @param {String} responseStr  送信時のレスポンスをテキスト出力したもの
  * @param {Array<String>} uploadedFileData  アップロードしたファイルの情報が格納される配列
  */
function addOutputToArray(responseStr,uploadedFileData){
  const json = JSON.parse(responseStr);
  uploadedFileData[0].push(json.entries[0].id);
  uploadedFileData[1].push(`https://app.box.com/file/${json.entries[0].id}`);
}
/**
  * アップロードしたデータのパスとURLをデータ項目に出力する。
  * @param {ProcessDataDefinitionView} dataDef  データ項目の ProcessDataDefinitionView
  * @param {Array<String>} uploadedFileData  アップロードしたファイルの情報が格納されている配列
  */
function setData(dataDef,uploadedFileData){
  if(dataDef !==  null){
    engine.setData(dataDef,uploadedFileData.join('\n'));
  }
}

Download

Capture

Notes

  1. フォルダ ID は、URL に含まれています。https://{sub-domain}.app.box.com/folder/(Folder ID)
  2. ファイル名が競合(コンフリクト)する場合はエラーとなります。このとき、競合したファイルが50MBより大きい場合はすべてのファイルがアップロードされませんが、50MB以下の場合はそれより前にセットされているファイルがアップロードされます。
  3. Box のリフレッシュトークンには、期限があります。期限を超えないよう、定期的に利用する必要があります。(2019 年 7 月時点で、60日。 https://box-content.readme.io/docs/oauth-20)
  4. 50MBよりも大きなファイルはBoxの指定するサイズに分割して送信されます。この場合必要な通信の回数は(ファイルサイズ÷指定サイズ)+2 となります(カッコ内は切り上げ)。

%d人のブロガーが「いいね」をつけました。