背景
Box(https://www.box.com/home)是定义为内容云,在我有限认知里面,感觉应该和云存储系统没啥区别。近日,有幸和Box做了一次浅度接触,颇为缠绵,记录在这里供有需要的朋友参考。
需求很简单,合作伙伴会使用Box存储一些文件用于我们之间共享协作,我们要能够通过编程方式实现Box上资料的文件上传和下载。
云存储产品之前接触过一些,起初不以为然,但体验过程中就遇到了一些周折。在国内云存储已经和阳光、空气和水一样无处不在的大环境下,恐怕很少有必须使用国外的云存储产品,因为在使用过程中,几乎找不到有价值的参考资料,于是,在这里做个总结,以供有需求的朋友能够对基于Box的存储服务能够快速的入门。为了简单直观,文中使用了大量的截图,并且在过程中引用了大量外部链接供您参考。
Box相关资料
Box官网:https://www.box.com/home
Box开发者网站(文档和支持):https://developer.box.com/get-started/
Box开发者Console:https://app.box.com/developers/services
Box Java SDK github:https://github.com/box/box-java-sdk
如何通过Service Account访问Box文件夹:https://support.box.com/hc/en-us/community/posts/1500000023641-Explain-How-to-Access-Files-via-a-Service-Account
前序工作
首先注册账号,申请一个免费的个人账号体验一下,这里没什么可说的。
选择free的版本就好了
输入全名和邮箱以及密码和确认密码,之后通过邮箱激活。一切轻松搞定
然后就是signin
成功登录后
Box会在你的存储空间的根目录下预存一个文件Get Started with Box.pdf,没时间去看,有兴趣的可以了解一下,应该包含特性的介绍已经你能在这里做什么的说明。但我要做的通过编程去实现文件上传下载,虽然简单到不行,但是这种文档应该也不会提供此类内容。
接下来的部分就是看如何通过编程接口来实现文件上传下载了。按习惯,应该找开发者文档之类的东西。去研究一下这个地址吧。https://developer.box.com/get-started/
看到这里,不得不叹服于Box产品文档的强大API接口,甚至还专门讲解了如何通过Postman去调用API,但别着急去看那么强大的文档,向下翻翻
还是这个GetStart页面拖到底部,发现有各种开发语言的SDK可用,好事啊
有支持各种编程语言的SDK可以直接用。妥妥滴。
作为主营Java业务的程序员,直接去看看JavaSDK吧 https://github.com/box/box-java-sdk
开发者Console地址:https://app.box.com/developers/services
这里是中文,如果有必要,可以在Box“我的账户”下设置语言,如果看到截图中有中文又有英文的,那我就是在这里可以切换语言设置了。因为开发者文档中很多术语翻译过来在中文环境下难以对应。
进入正题
目标:协作方会提供一个文件夹,用于存储文件,我方需要通过编程实现Box的文件上传下载,这一切只需要在服务端完成。
1、创建应用
首先需要在开发者Console创建一个应用。
1.1 选择应用类型
Box允许创建3中类型的应用:
- 自定义应用程序;
- 受限访问应用程序;
- Box自定义技能。
对于每种类型的应用不在这里详细说明了,如果想了解详情,请查看相关的说明文档。
根据我们的需求背景,选择自定义应用类型;
1.2 选择认证方式
对于自定义应用程序类型的应用,Box提供了3种认证方式:
- 使用JWT的服务器端验证;
- OAuth2.0的用户验证;
- 基于App Token的服务器端认证。
这里最好切换成英文的,因为翻译成中文后对照开发者文档(只提供了英文和日文版)看起来有点糊涂。这里根据需求,选择使用JWT的服务端认证。然后在下方输入应用名称即可。
2、获取用于认证的秘钥对
在应用的配置(Configuration)选项卡页中“添加和管理公钥”(Add and Manage Public Keys)直接使用“生成公私秘钥对”(Generate a Public/Private Keypair)下载一个配置文件xxx_config.json
这里需要一个前置条件,首先要求账户必须启动 two factor 认证。
2.1 插曲:two factor认证
这里需要返回到“我的账户”进行two factor认证设置。
在“我的账户”下的“账户设置”中设置“2-Step Verification”
点击“Set up”
这里又出现了两个选择,选择何种方式做两步校验:
- 基于App的认证方式;
- 基于SMS短信的验证。
这里选择基于App的认证,这也是推荐的认证方式,因为在国内短信服务经常无效,这个坑我已经踩过,希望你不要再踩了。老老实实的下载一个认证的手机App,我选择了微软认证器(Microsoft Authenticator),当然你也可以选择其他认证器,iPhone用户在AppStore搜索“认证器”就能找到 Microsoft Authenticator,下载安装。
用Microsoft Authenticator扫描二维码,会在Microsoft Authenticator上获取一个30秒消失用于的一次性验证的数字码,在网页的二维码下面Step2中输入这个验证码。
到这里,two factor 认证就算完成了。
在回到开发者Console继续完成密钥对的生成工作。再次点击“生成公/私密钥对”(Generate a Public/Private Keypair),这是页面跳出要求输入二步验证的页面,再次打开微软认证器的手机App,输入认证码,拼手速,因为认证码30秒刷新一次。
认证通过,再次点击“Generate a Public/Private Keypair”
下载xxx_config.json配置文件
这个配置文件就可以放在你的工程中了,但你以为这样就完了?并没有
3、申请授权
在开发者Console的应用配置中给应用做一下授权,默认只有读权限,这里给个写权限。
管理权限因为需求用不上,这里不给了。然后保存设置即可。
再切换到“Authorization”选项卡,要通过应用访问Box资源需要提交授权申请,然后由管理员批准授权方可通过应用使用编程方式访问Box资源。
提交授权申请
切换到管理员Console
在“Apps”->"Custom Apps"下可以看到授权申请,这里批准授权即可
授权应用
授权后,授权状态变为Enabled
再次切换到开发者Console,查看应用的授权状态,已经变为Enbaled,证明授权已经通过。
现在可以通过程序调用了。
这里,还需要交代一点,通过应用访问Box,相当于在Box中新建了一个自动创建的服务账户(Service Account),关于这一点的详细说明如下:
https://developer.box.com/guides/authentication/user-types/service-account/
需要说明的是,服务账户(Service Account)在Box中相当于一个托管的账户,不能用于登录Box系统,只能通过API调用Box的服务。服务账户默认只能访问本服务账户下的文件夹和文件,不能访问到其他的文件夹和文件。如果要访问其他的文件夹,需要主账号在自己的文件夹上添加合作者。添加合作者的方式就是添加这个Service Account的email为合作者,关于这个问题参考
这个问题的答复。
4、添加合作者
如上,我们首先要找到这个Service Account的email,然后为主账号下的文件夹添加合作者。
Service Account的email在开发者Console中的应用General Settings中查看 Service Account Info中查看
类似 AutomationUser_xxx_xxxxx@boxdevedition.com的email地址就是这个Service Account的email,“美们,复制它!”。
返回“我的账户”,选择要分享给Service Account的文件夹,这里以/books这个文件夹为例,点击进入books文件夹
点击右侧的Sharing->Collaborators->“Invite People”
在Invite People中添加之前复制的Service Account的email
添加成功
到了这里,就可以通过编程调用Box接口获取共享文件夹的访问权限,之后遍历文件夹,上传文件,下载文件就可以实现了。
5、编码
注意,之前生成并下载的密钥对配置文件放到工程的resource目录中,以便在编译后可以在classpath中可访问。
写个工具类,实现了Box文件夹遍历、文件上传、文件下载的功能,供参考
import com.box.sdk.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* Box 工具类
*
*/
public class BoxUtils {
private final static Logger logger = LoggerFactory.getLogger(BoxUtils.class);
/**
* 上传文件到指定的Box文件夹
*
* @param api BoxAPIConnection 不能为空
* @param boxFolderId 文件夹ID,不能为空
* @param localFile 本地文件对象,不能为空
* @param targetFileName 目标文件名
* @param targetFileDesc 目标文件描述
* @param listener 进度监听器
* @return BoxFile.Info
* @throws FileNotFoundException 打开文件输入流文件不存在会抛出异常
*/
public static BoxFile.Info uploadFile(BoxAPIConnection api, String boxFolderId, File localFile, String targetFileName, String targetFileDesc, ProgressListener listener) throws FileNotFoundException {
if (api == null) {
logger.error("BoxAPIConnection should not be null");
}
if (boxFolderId == null || boxFolderId.trim().equals("")) {
logger.error("boxFolderId should not be empty");
}
BoxFolder folder = new BoxFolder(api, boxFolderId);
if (localFile.isFile() && localFile.exists()) {
long localFileSize = localFile.length();
targetFileName = targetFileName == null || targetFileName.equals("") ? localFile.getName() : targetFileName;
folder.canUpload(targetFileName, localFileSize);
FileInputStream localFin = new FileInputStream(localFile);
FileUploadParams params = new FileUploadParams();
params.setContent(localFin);
params.setName(targetFileName);
params.setDescription(targetFileDesc);
params.setProgressListener(listener);
return folder.uploadFile(params);
} else {
logger.error("localFilePath is not exist or is not file");
return null;
}
}
/**
* 上传文件到指定的Box文件夹
*
* @param api BoxAPIConnection 不能为空
* @param boxFolderId 文件夹ID,不能为空
* @param localFilePath 本地文件路径,不能为空
* @param targetFileName 目标文件名
* @param targetFileDesc 目标文件描述
* @param listener 进度监听器
* @return BoxFile.Info
* @throws FileNotFoundException 打开文件输入流文件不存在会抛出异常
*/
public static BoxFile.Info uploadFile(BoxAPIConnection api, String boxFolderId, String localFilePath, String targetFileName, String targetFileDesc, ProgressListener listener) throws FileNotFoundException {
if (localFilePath == null || localFilePath.trim().equals("")) {
logger.error("localFilePath can't be empty");
}
File localFile = new File(localFilePath);
return uploadFile(api, boxFolderId, localFile, targetFileName, targetFileDesc, listener);
}
/**
* 上传文件到指定的Box文件夹
*
* @param api BoxAPIConnection 不能为空
* @param boxFolderId Box 文件夹ID,不能为空
* @param localFilePath 本地文件路径,不能为空
* @return BoxFile.Info
* @throws FileNotFoundException 打开文件输入流文件不存在会抛出异常
*/
public static BoxFile.Info uploadFile(BoxAPIConnection api, String boxFolderId, String localFilePath) throws FileNotFoundException {
return uploadFile(api, boxFolderId, localFilePath, null, null, null);
}
/**
* 上传文件到指定的Box文件夹
*
* @param api BoxAPIConnection 不能为空
* @param boxFolderId Box 文件夹ID,不能为空
* @param localFile 本地文件路径,不能为空
* @return BoxFile.Info
* @throws FileNotFoundException 打开文件输入流文件不存在会抛出异常
*/
public static BoxFile.Info uploadFile(BoxAPIConnection api, String boxFolderId, File localFile) throws FileNotFoundException {
return uploadFile(api, boxFolderId, localFile, null, null, null);
}
/**
* 下载文件
* 输出流会在方法中被关闭 localOutputStream.close();
*
* @param api BoxAPIConnection 不能为空
* @param boxFileId Box 文件ID,不能为空
* @param localOutputStream 下载文件的目标输出流,不能为空
* @param listener 进度监听器
* @throws IOException
*/
public static void downloadFile(BoxAPIConnection api, String boxFileId, OutputStream localOutputStream, ProgressListener listener) throws IOException {
BoxFile file = new BoxFile(api, boxFileId);
if (listener != null) {
file.download(localOutputStream, listener);
} else {
file.download(localOutputStream);
}
localOutputStream.close();
}
/**
* 下载文件
* 输出流会在方法中被关闭 localOutputStream.close();
*
* @param api BoxAPIConnection 不能为空
* @param boxFileId Box 文件ID,不能为空
* @param localOutputStream 下载文件的目标输出流,不能为空
* @throws IOException
*/
public static void downloadFile(BoxAPIConnection api, String boxFileId, OutputStream localOutputStream) throws IOException {
downloadFile(api, boxFileId, localOutputStream, null);
}
/**
* 下载文件
*
* @param api BoxAPIConnection 不能为空
* @param boxFileId Box 文件ID,不能为空
* @param localFilePath 下载文件的本地文件路径,不能为空
* @param listener 进度监听器
* @throws IOException
*/
public static void downloadFile(BoxAPIConnection api, String boxFileId, String localFilePath, ProgressListener listener) throws IOException {
FileOutputStream localOutputStream = new FileOutputStream(new File(localFilePath));
downloadFile(api, boxFileId, localOutputStream, listener);
}
/**
* 下载文件
*
* @param api BoxAPIConnection 不能为空
* @param boxFileId Box 文件ID,不能为空
* @param localFilePath 下载文件的本地文件路径,不能为空
* @throws IOException
*/
public static void downloadFile(BoxAPIConnection api, String boxFileId, String localFilePath) throws IOException {
FileOutputStream localOutputStream = new FileOutputStream(new File(localFilePath));
downloadFile(api, boxFileId, localOutputStream);
}
/**
* 下载文件到指定本地目录,文件名同box文件名
*
* @param api BoxAPIConnection 不能为空
* @param boxFileId Box 文件ID,不能为空
* @param localDir 下载文件的本地目录,不能为空
* @param listener 进度监听器
* @throws IOException
*/
public static void downloadFileToDir(BoxAPIConnection api, String boxFileId, String localDir, ProgressListener listener) throws IOException {
BoxFile boxFile = new BoxFile(api, boxFileId);
BoxFile.Info info = boxFile.getInfo();
String fileName = info.getName();
String locaFilePath = null;
if (localDir.endsWith("/") || localDir.endsWith("\\")) {
locaFilePath = localDir + fileName;
} else {
locaFilePath = localDir + File.separator + fileName;
}
downloadFile(api, boxFileId, locaFilePath, listener);
}
/**
* 下载文件到指定本地目录,文件名同box文件名
*
* @param api BoxAPIConnection 不能为空
* @param boxFileId Box 文件ID,不能为空
* @param localDir 下载文件的本地目录,不能为空
* @throws IOException
*/
public static void downloadFileToDir(BoxAPIConnection api, String boxFileId, String localDir) throws IOException {
downloadFileToDir(api, boxFileId, localDir, null);
}
/**
* 遍历指定文件夹中的所有文件及文件夹
*
* @param api BoxAPIConnection 不能为空
* @param folderId Box 文件夹ID,如果为空,或者是"/"代表遍历根目录;否则遍历指定文件夹
* @return
*/
public static List<BoxItem.Info> listFiles(BoxAPIConnection api, String folderId) {
BoxFolder folder = null;
if (folderId == null || folderId.trim().equals("") || folderId.trim().equals("/")) {
folder = BoxFolder.getRootFolder(api);
} else {
folder = new BoxFolder(api, folderId);
}
List<BoxItem.Info> list = new ArrayList<BoxItem.Info>();
for (BoxItem.Info itemInfo : folder) {
list.add(itemInfo);
}
return list;
}
}
写个demo
import com.box.sdk.*;
import java.io.*;
import java.util.List;
public class Demo {
private final static String FOLDER_ID = "xxx";
private static BoxConfig getBoxConfig() throws IOException {
InputStream inputStream = Demo.class.getClassLoader().getResourceAsStream("config.json");
Reader reader = new InputStreamReader(inputStream);
BoxConfig config = BoxConfig.readFrom(reader);
return config;
}
private static BoxDeveloperEditionAPIConnection getApi() throws IOException {
BoxConfig boxConfig = getBoxConfig();
BoxDeveloperEditionAPIConnection api = BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(boxConfig);
return api;
}
public static void main(String[] a) throws IOException {
BoxAPIConnection api = getApi();
//遍历root目录
List<BoxItem.Info> files = BoxUtils.listFiles(api,"/");
for (BoxItem.Info file:files){
System.out.println(file.getID()+"::"+file.getName());
}
//创建文件夹
createRootFolder(api,"books");
//上传文件
BoxUtils.uploadFile(api,FOLDER_ID,"/your/local/file.txt");
//下载文件
BoxUtils.downloadFile(api,"boxFileId","/your/local/file.txt");
//下载文件到文件夹
BoxUtils.downloadFileToDir(api,"boxFileId","/your/local/folder");
}
public static void createRootFolder(BoxAPIConnection api,String folderName){
BoxFolder rootFolder = BoxFolder.getRootFolder(api);
BoxFolder.Info folderInfo = rootFolder.createFolder(folderName);
System.out.println(folderInfo.getName());
}
}
以上,基本还原看了Box从注册、配置、开发编码的整个过程。