Box: Upload File
Upload files to a specified folder on Box.
2020-10-06 (C) Questetra, Inc. (MIT License)
Configs
  • C1: OAuth2 Setting Name *
  • C2: File type data item whose attached files will be uploaded *
  • C3: Folder ID that files will be uploaded (Root folder if blank) #{EL}
  • C4: String type data item that will save uploaded file ids
  • C5: String type data item that will save uploaded file urls
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. Folder ID is contained in the URL: https://{sub-domain}.app.box.com/folder/(Folder ID)
  2. If there are filename conflicts it will result in an error. When the conflicted file is over 50MB, no files will be uploaded. When it is 50MB or under, files set before it will be uploaded
  3. Box refresh tokens have an expiration date. You must use them regularly to ensure that the expiration date is not exceeded. (60 days as of July 2019. https://box-content.readme.io/docs/oauth-20)
  4. A File over 50MB will be divided into parts with the size of every part specified by Box. In this case, number of sending requests is expressed by (file size ÷ specified size) + 2 (you should round up the result of the calculation in parenthesis).

See also

%d bloggers like this: