mirror of
https://github.com/zfile-dev/zfile.git
synced 2025-04-19 05:34:52 +00:00
✨ 增加 Google Drive 支持
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package im.zhaojun.zfile.admin.model.param;
|
||||
|
||||
import im.zhaojun.zfile.admin.annotation.StorageParamItem;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Google Drive 初始化参数
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
@ToString
|
||||
public class GoogleDriveParam extends ProxyTransferParam {
|
||||
|
||||
@StorageParamItem(name = "clientId", defaultValue = "${zfile.gd.clientId}", order = 1, description = "默认 API 仅用作示例,因审核原因,目前不可用,请自行申请 API", link = "https://docs.zfile.vip/advanced#google-drive-api", linkName = "自定义 API 文档")
|
||||
private String clientId;
|
||||
|
||||
@StorageParamItem(name = "SecretKey", defaultValue = "${zfile.gd.clientSecret}", order = 2)
|
||||
private String clientSecret;
|
||||
|
||||
@StorageParamItem(name = "回调地址", description = "这里要修改为自己的域名", defaultValue = "${zfile.gd.redirectUri}", order = 3)
|
||||
private String redirectUri;
|
||||
|
||||
@Setter
|
||||
@StorageParamItem(name = "访问令牌", link = "/gd/authorize", linkName = "前往获取令牌", order = 4)
|
||||
private String accessToken;
|
||||
|
||||
@Setter
|
||||
@StorageParamItem(name = "刷新令牌", order = 5)
|
||||
private String refreshToken;
|
||||
|
||||
@StorageParamItem(name = "网盘", order = 6, required = false)
|
||||
private String driveId;
|
||||
|
||||
@StorageParamItem(name = "基路径", defaultValue = "/", order = 7, description = "基路径表示读取的根文件夹,不填写表示允许读取所有。如: '/','/文件夹1'")
|
||||
private String basePath;
|
||||
|
||||
@StorageParamItem(name = "加速域名", required = false, description = "可使用 cf worker index 程序的链接,会使用 cf 中转下载,教程自行查询. 不填写则使用服务器中转下载.")
|
||||
private String domain;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package im.zhaojun.zfile.admin.model.request.gd;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value="gd drive 列表请求类")
|
||||
public class GetGdDriveListRequest {
|
||||
|
||||
@NotBlank(message = "accessToken 不能为空")
|
||||
@ApiModelProperty(value = "accessToken", required = true, example = "v7LtfjIbnxLCTj0R3riwhyxcbv4KVH5HuPWHWrrewHMEwjJyUlYXV6D4m1MLJ2dP__GX_7CKCc-HudUetPXWS2wwbfkNs6ydLq3xrk1gHA7wcD_pmt6oNuRXw5mnFzfdLkH5wIG1suQp3p0eHJurzIaCgYKATASATASFQE65dr8hO725r41QtZc9RJVUg12cA0163")
|
||||
private String accessToken;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package im.zhaojun.zfile.admin.model.result.gd;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* gd drive 基本信息结果类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@ApiModel(value="gd drive 基本信息结果类")
|
||||
public class GdDriveInfoResult {
|
||||
|
||||
@ApiModelProperty(value = "drive id", example = "0AGrY0xF1D7PEUk9PVB")
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty(value = "drive 名称", example = "zfile")
|
||||
private String name;
|
||||
|
||||
}
|
||||
@@ -204,7 +204,11 @@ public class StorageSourceContext implements ApplicationContextAware {
|
||||
* @return 存储源对应的 Service
|
||||
*/
|
||||
public AbstractBaseFileService<?> getByKey(String key) {
|
||||
return get(storageSourceService.findIdByKey(key));
|
||||
Integer storageId = storageSourceService.findIdByKey(key);
|
||||
if (storageId == null) {
|
||||
return null;
|
||||
}
|
||||
return get(storageId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package im.zhaojun.zfile.common.controller.callback;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import im.zhaojun.zfile.admin.model.dto.OAuth2Token;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Api(tags = "Google Drive 认证回调模块")
|
||||
@Controller
|
||||
@Slf4j
|
||||
@RequestMapping(value = {"/gd"})
|
||||
public class GdCallbackController {
|
||||
|
||||
@Value("${zfile.gd.clientId}")
|
||||
private String clientId;
|
||||
|
||||
@Value("${zfile.gd.redirectUri}")
|
||||
private String redirectUri;
|
||||
|
||||
@Value("${zfile.gd.clientSecret}")
|
||||
private String clientSecret;
|
||||
|
||||
@Value("${zfile.gd.scope}")
|
||||
private String scope;
|
||||
|
||||
@GetMapping("/authorize")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@ApiOperation(value = "生成 OAuth2 登陆 URL", notes = "生成 OneDrive OAuth2 登陆 URL,用户国际版,家庭版等非世纪互联运营的 OneDrive.")
|
||||
public String authorize(String clientId, String clientSecret, String redirectUri) {
|
||||
log.info("gd 生成授权链接参数信息: clientId: {}, clientSecret: {}, redirectUri: {}", clientId, clientSecret, redirectUri);
|
||||
|
||||
if (StrUtil.isAllEmpty(clientId, clientSecret, redirectUri)) {
|
||||
clientId = this.clientId;
|
||||
redirectUri = this.redirectUri;
|
||||
clientSecret = this.clientSecret;
|
||||
}
|
||||
|
||||
String stateStr = "&state=" + Base64.encodeUrlSafe(StrUtil.join("::", clientId, clientSecret, redirectUri));
|
||||
|
||||
String authorizeUrl = "https://accounts.google.com/o/oauth2/v2/auth?client_id=" + clientId
|
||||
+ "&response_type=code&redirect_uri=" + redirectUri
|
||||
+ "&scope=" + this.scope
|
||||
+ "&access_type=offline"
|
||||
+ stateStr;
|
||||
|
||||
log.info("gd 生成授权链接结果: {}", authorizeUrl);
|
||||
|
||||
return "redirect:" + authorizeUrl;
|
||||
}
|
||||
|
||||
@GetMapping("/callback")
|
||||
public String gdCallback(String code, String state, Model model) {
|
||||
log.info("gd 授权回调参数信息: code: {}, state: {}", code, state);
|
||||
|
||||
String clientId, clientSecret, redirectUri;
|
||||
|
||||
if (StrUtil.isEmpty(state)) {
|
||||
clientId = this.clientId;
|
||||
clientSecret = this.clientSecret;
|
||||
redirectUri = this.redirectUri;
|
||||
} else {
|
||||
String stateDecode = Base64.decodeStr(state);
|
||||
String[] stateArr = stateDecode.split("::");
|
||||
clientId = stateArr[0];
|
||||
clientSecret = stateArr[1];
|
||||
redirectUri = stateArr[2];
|
||||
}
|
||||
|
||||
final String uri = "https://accounts.google.com/o/oauth2/token";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
String clientCredentials = Base64.encodeUrlSafe(clientId + ":" + clientSecret);
|
||||
headers.add("Authorization", "Basic " + clientCredentials);
|
||||
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
|
||||
requestBody.add("code", code);
|
||||
requestBody.add("grant_type", "authorization_code");
|
||||
requestBody.add("redirect_uri", redirectUri);
|
||||
requestBody.add("scope", scope);
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> formEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
NoRedirectClientHttpRequestFactory noRedirectClientHttpRequestFactory = new NoRedirectClientHttpRequestFactory();
|
||||
|
||||
ResponseEntity<String> response = new RestTemplate(noRedirectClientHttpRequestFactory).exchange(uri, HttpMethod.POST, formEntity, String.class);
|
||||
|
||||
String body = response.getBody();
|
||||
log.info("{} 根据授权回调 code 获取令牌结果:body: {}", this, body);
|
||||
|
||||
OAuth2Token oAuth2Token ;
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
oAuth2Token = OAuth2Token.fail(clientId, clientSecret, redirectUri, body);
|
||||
} else {
|
||||
JSONObject jsonBody = JSONObject.parseObject(body);
|
||||
String accessToken = jsonBody.getString("access_token");
|
||||
String refreshToken = jsonBody.getString("refresh_token");
|
||||
oAuth2Token =
|
||||
OAuth2Token.success(clientId, clientSecret, redirectUri, accessToken, refreshToken, body);
|
||||
}
|
||||
|
||||
model.addAttribute("oauth2Token", oAuth2Token);
|
||||
model.addAttribute("type", "Google Drive");
|
||||
return "callback";
|
||||
}
|
||||
|
||||
static class NoRedirectClientHttpRequestFactory extends
|
||||
SimpleClientHttpRequestFactory {
|
||||
|
||||
@Override
|
||||
protected void prepareConnection(HttpURLConnection connection,
|
||||
String httpMethod) throws IOException {
|
||||
super.prepareConnection(connection, httpMethod);
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package im.zhaojun.zfile.common.controller.gd;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import im.zhaojun.zfile.admin.model.request.gd.GetGdDriveListRequest;
|
||||
import im.zhaojun.zfile.admin.model.result.gd.GdDriveInfoResult;
|
||||
import im.zhaojun.zfile.common.util.AjaxJson;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Api(tags = "gd 工具辅助模块")
|
||||
@Controller
|
||||
@RequestMapping("/gd")
|
||||
public class GDHelperController {
|
||||
|
||||
@PostMapping("/drives")
|
||||
@ResponseBody
|
||||
@ApiOperationSupport(order = 1)
|
||||
@ApiOperation(value = "获取 gd drives 列表")
|
||||
public AjaxJson<List<GdDriveInfoResult>> getDrives(@Valid @RequestBody GetGdDriveListRequest gdDriveListRequest) {
|
||||
List<GdDriveInfoResult> bucketNameList = new ArrayList<>();
|
||||
String accessToken = gdDriveListRequest.getAccessToken();
|
||||
|
||||
HttpRequest httpRequest = HttpUtil.createGet("https://www.googleapis.com/drive/v3/drives");
|
||||
httpRequest.header("Authorization", "Bearer " + accessToken);
|
||||
|
||||
HttpResponse httpResponse = httpRequest.execute();
|
||||
|
||||
String body = httpResponse.body();
|
||||
JSONObject jsonObject = JSON.parseObject(body);
|
||||
JSONArray drives = jsonObject.getJSONArray("drives");
|
||||
|
||||
for (int i = 0; i < drives.size(); i++) {
|
||||
JSONObject drive = drives.getJSONObject(i);
|
||||
String id = drive.getString("id");
|
||||
String name = drive.getString("name");
|
||||
bucketNameList.add(new GdDriveInfoResult(id, name));
|
||||
}
|
||||
|
||||
return AjaxJson.getSuccessData(bucketNameList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,4 +29,9 @@ public class StorageSourceException extends RuntimeException {
|
||||
this.storageId = storageId;
|
||||
}
|
||||
|
||||
public StorageSourceException(Integer storageId, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.storageId = storageId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -105,5 +105,8 @@ public class StorageSourceAllParam {
|
||||
|
||||
@ApiModelProperty(value = "编码格式", example = "UTF-8")
|
||||
private String encoding;
|
||||
|
||||
|
||||
@ApiModelProperty(value = "存储源 ID", example = "0AGrY0xF1D7PEUk9PV2")
|
||||
private String driveId;
|
||||
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public enum StorageTypeEnum implements IEnum {
|
||||
ONE_DRIVE_CHINA("onedrive-china", "OneDrive 世纪互联"),
|
||||
SHAREPOINT_DRIVE("sharepoint", "SharePoint"),
|
||||
SHAREPOINT_DRIVE_CHINA("sharepoint-china", "SharePoint 世纪互联"),
|
||||
GOOGLE_DRIVE("google-drive", "Google Drive"),
|
||||
QINIU("qiniu", "七牛云 KODO");
|
||||
|
||||
private static final Map<String, StorageTypeEnum> ENUM_MAP = new HashMap<>();
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
package im.zhaojun.zfile.home.service.impl;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import im.zhaojun.zfile.admin.constant.StorageConfigConstant;
|
||||
import im.zhaojun.zfile.admin.model.dto.OAuth2Token;
|
||||
import im.zhaojun.zfile.admin.model.entity.StorageSourceConfig;
|
||||
import im.zhaojun.zfile.admin.model.param.GoogleDriveParam;
|
||||
import im.zhaojun.zfile.admin.service.StorageSourceConfigService;
|
||||
import im.zhaojun.zfile.common.cache.RefreshTokenCache;
|
||||
import im.zhaojun.zfile.common.exception.StorageSourceRefreshTokenException;
|
||||
import im.zhaojun.zfile.common.exception.file.StorageSourceException;
|
||||
import im.zhaojun.zfile.common.util.RequestHolder;
|
||||
import im.zhaojun.zfile.common.util.StringUtils;
|
||||
import im.zhaojun.zfile.home.model.enums.FileTypeEnum;
|
||||
import im.zhaojun.zfile.home.model.enums.StorageTypeEnum;
|
||||
import im.zhaojun.zfile.home.model.result.FileItemResult;
|
||||
import im.zhaojun.zfile.home.service.base.ProxyTransferService;
|
||||
import im.zhaojun.zfile.home.service.base.RefreshTokenService;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Service
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Slf4j
|
||||
public class GoogleDriveServiceImpl extends ProxyTransferService<GoogleDriveParam> implements RefreshTokenService {
|
||||
|
||||
/**
|
||||
* 文件类型:文件夹
|
||||
*/
|
||||
private static final String FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
|
||||
|
||||
/**
|
||||
* 文件类型:快捷方式
|
||||
*/
|
||||
private static final String SHORTCUT_MIME_TYPE = "application/vnd.google-apps.shortcut";
|
||||
|
||||
/**
|
||||
* 文件基础操作 API
|
||||
*/
|
||||
private static final String DRIVE_FILE_URL = "https://www.googleapis.com/drive/v3/files";
|
||||
|
||||
|
||||
/**
|
||||
* 文件上传操作 API
|
||||
*/
|
||||
private static final String DRIVE_FILE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
|
||||
|
||||
/**
|
||||
* 刷新 AccessToken URL
|
||||
*/
|
||||
private static final String REFRESH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
|
||||
@javax.annotation.Resource
|
||||
private StorageSourceConfigService storageSourceConfigService;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
refreshAccessToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取文件/文件夹 id
|
||||
*
|
||||
* @param path
|
||||
* 路径
|
||||
*
|
||||
* @return 文件/文件夹 id
|
||||
*/
|
||||
private String getIdByPath(String path) {
|
||||
String fullPath = StringUtils.concat(param.getBasePath(), path);
|
||||
if (StrUtil.isEmpty(fullPath) || StrUtil.equals(fullPath, "/")) {
|
||||
return StrUtil.isEmpty(param.getDriveId()) ? "root" : param.getDriveId();
|
||||
}
|
||||
|
||||
List<String> pathList = StrUtil.splitTrim(fullPath, "/");
|
||||
|
||||
String driveId = "";
|
||||
for (String subPath : pathList) {
|
||||
String folderIdParam = new GoogleDriveAPIParam().getDriveIdByPathParam(subPath, driveId);
|
||||
HttpRequest httpRequest = HttpUtil.createGet(DRIVE_FILE_URL);
|
||||
httpRequest.header("Authorization", "Bearer " + param.getAccessToken());
|
||||
httpRequest.body(folderIdParam);
|
||||
|
||||
HttpResponse httpResponse = httpRequest.execute();
|
||||
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("根据文件夹路径获取文件夹id失败, storageId: {},folderPath: {}, httpResponse body: {}", storageId, path, httpResponse.body());
|
||||
throw new StorageSourceException(storageId, "文件目录不存在或请求异常[1]");
|
||||
}
|
||||
|
||||
String body = httpResponse.body();
|
||||
|
||||
JSONObject jsonObject = JSON.parseObject(body);
|
||||
JSONArray files = jsonObject.getJSONArray("files");
|
||||
|
||||
if (files.size() == 0) {
|
||||
throw new StorageSourceException(storageId, "文件目录不存在或请求异常[2]");
|
||||
}
|
||||
|
||||
driveId = files.getJSONObject(0).getString("id");
|
||||
}
|
||||
|
||||
return driveId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FileItemResult> fileList(String folderPath) throws Exception {
|
||||
List<FileItemResult> result = new ArrayList<>();
|
||||
|
||||
String folderId = getIdByPath(folderPath);
|
||||
String pageToken = "";
|
||||
do {
|
||||
String folderIdParam = new GoogleDriveAPIParam().getFileListParam(folderId, pageToken);
|
||||
HttpRequest httpRequest = HttpUtil.createGet(DRIVE_FILE_URL);
|
||||
httpRequest.header("Authorization", "Bearer " + param.getAccessToken());
|
||||
httpRequest.body(folderIdParam);
|
||||
|
||||
HttpResponse httpResponse = httpRequest.execute();
|
||||
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("根据文件夹路径获取文件夹列表失败, storageId: {},folderPath: {}, httpResponse body: {}", storageId, folderPath, httpResponse.body());
|
||||
throw new StorageSourceException(storageId, "文件目录不存在或请求异常[3]");
|
||||
}
|
||||
|
||||
String body = httpResponse.body();
|
||||
|
||||
JSONObject jsonObject = JSON.parseObject(body);
|
||||
pageToken = jsonObject.getString("nextPageToken");
|
||||
JSONArray files = jsonObject.getJSONArray("files");
|
||||
result.addAll(jsonArrayToFileList(files, folderPath));
|
||||
} while (StrUtil.isNotEmpty(pageToken));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileItemResult getFileItem(String pathAndName) {
|
||||
String fileId = getIdByPath(pathAndName);
|
||||
|
||||
String folderName = FileUtil.getParent(pathAndName, 1);
|
||||
|
||||
HttpRequest httpRequest = HttpUtil.createGet(DRIVE_FILE_URL + "/" + fileId);
|
||||
httpRequest.header("Authorization", "Bearer " + param.getAccessToken());
|
||||
httpRequest.body("fields=id,name,mimeType,shortcutDetails,size,modifiedTime");
|
||||
HttpResponse httpResponse = httpRequest.execute();
|
||||
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("根据文件路径获取文件详情失败, storageId: {},pathAndName: {}, httpResponse body: {}", storageId, pathAndName, httpResponse.body());
|
||||
throw new StorageSourceException(storageId, "文件不存在或请求异常");
|
||||
}
|
||||
|
||||
String body = httpResponse.body();
|
||||
JSONObject jsonObject = JSON.parseObject(body);
|
||||
return jsonObjectToFileItem(jsonObject, folderName);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean newFolder(String path, String name) {
|
||||
HttpResponse httpResponse = HttpRequest.post(DRIVE_FILE_URL)
|
||||
.header("Authorization", "Bearer " + param.getAccessToken())
|
||||
.body(new JSONObject()
|
||||
.fluentPut("name", name)
|
||||
.fluentPut("mimeType", FOLDER_MIME_TYPE)
|
||||
.fluentPut("parents", Collections.singletonList(getIdByPath(path)))
|
||||
.toJSONString())
|
||||
.execute();
|
||||
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("新建文件夹失败, storageId: {},path: {}, name: {}, httpResponse body: {}", storageId, path, name, httpResponse.body());
|
||||
throw new StorageSourceException(storageId, "新建文件夹失败");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFile(String path, String name) {
|
||||
String pathAndName = StringUtils.concat(path, name);
|
||||
HttpResponse httpResponse = HttpRequest.delete(DRIVE_FILE_URL + "/" + getIdByPath(pathAndName))
|
||||
.header("Authorization", "Bearer " + param.getAccessToken())
|
||||
.execute();
|
||||
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("删除文件/文件夹失败, storageId: {},pathAndName: {}, httpResponse body: {}", storageId, pathAndName, httpResponse.body());
|
||||
throw new StorageSourceException(storageId, "删除失败");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFolder(String path, String name) {
|
||||
return deleteFile(path, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean renameFile(String path, String name, String newName) {
|
||||
String pathAndName = StringUtils.concat(path, name);
|
||||
String fileId = getIdByPath(pathAndName);
|
||||
|
||||
HttpResponse httpResponse = HttpRequest.patch(DRIVE_FILE_URL + "/" + fileId)
|
||||
.header("Authorization", "Bearer " + param.getAccessToken())
|
||||
.body(new JSONObject()
|
||||
.fluentPut("name", newName)
|
||||
.toJSONString())
|
||||
.execute();
|
||||
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("重命名文件/文件夹失败, storageId: {},path: {}, name: {}, httpResponse body: {}", storageId, path, name, httpResponse.body());
|
||||
throw new StorageSourceException(storageId, "重命名文件/文件夹失败");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean renameFolder(String path, String name, String newName) {
|
||||
return renameFile(path, name, newName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadFile(String pathAndName, InputStream inputStream) {
|
||||
String boundary = IdUtil.fastSimpleUUID();
|
||||
String fileName = FileUtil.getName(pathAndName);
|
||||
String folderName = StringUtils.getParentPath(pathAndName);
|
||||
|
||||
String jsonString = new JSONObject()
|
||||
.fluentPut("name", fileName)
|
||||
.fluentPut("parents", Collections.singletonList(getIdByPath(folderName)))
|
||||
.toJSONString();
|
||||
HttpEntity entity = MultipartEntityBuilder.create()
|
||||
.setMimeSubtype("related")
|
||||
.setBoundary(boundary)
|
||||
.addTextBody(boundary, jsonString, ContentType.APPLICATION_JSON)
|
||||
.addBinaryBody(boundary, inputStream)
|
||||
.build();
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
HttpUriRequest httpUriRequest = RequestBuilder.post(DRIVE_FILE_UPLOAD_URL)
|
||||
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + param.getAccessToken())
|
||||
.setEntity(entity)
|
||||
.build();
|
||||
|
||||
CloseableHttpResponse response = httpClient.execute(httpUriRequest);
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
if (HttpStatus.valueOf(statusLine.getStatusCode()).isError()) {
|
||||
HttpEntity responseEntity = response.getEntity();
|
||||
String responseBody = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
|
||||
log.error("上传文件失败, storageId: {},pathAndName: {}, httpResponse body: {}", storageId, pathAndName, responseBody);
|
||||
throw new StorageSourceException(storageId, "上传文件失败:" + responseBody);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("上传文件失败, storageId: {},pathAndName: {}, httpResponse body: {}", storageId, pathAndName, e);
|
||||
throw new StorageSourceException(storageId, "上传文件失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Resource> downloadToStream(String pathAndName) {
|
||||
String fileId = getIdByPath(pathAndName);
|
||||
|
||||
HttpServletRequest request = RequestHolder.getRequest();
|
||||
|
||||
HttpRequest httpRequest = HttpUtil.createGet(DRIVE_FILE_URL + "/" + fileId);
|
||||
httpRequest.header("Authorization", "Bearer " + param.getAccessToken());
|
||||
httpRequest.body("alt=media");
|
||||
httpRequest.header(HttpHeaders.RANGE, request.getHeader(HttpHeaders.RANGE));
|
||||
HttpResponse httpResponse = httpRequest.executeAsync();
|
||||
if (HttpStatus.valueOf(httpResponse.getStatus()).isError()) {
|
||||
log.info("GoogleDrive下载文件失败, storageId: {},pathAndName: {}", storageId, pathAndName);
|
||||
throw new StorageSourceException(storageId, "下载文件失败");
|
||||
}
|
||||
|
||||
try {
|
||||
HttpServletResponse response = RequestHolder.getResponse();
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, httpResponse.header(HttpHeaders.CONTENT_RANGE));
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
response.setHeader(HttpHeaders.CONTENT_LENGTH, httpResponse.header(HttpHeaders.CONTENT_LENGTH));
|
||||
response.setContentType(httpResponse.header(HttpHeaders.CONTENT_TYPE));
|
||||
OutputStream outputStream = response.getOutputStream();
|
||||
httpResponse.writeBody(outputStream, true, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("下载文件失败:" + e.getMessage(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public StorageTypeEnum getStorageTypeEnum() {
|
||||
return StorageTypeEnum.GOOGLE_DRIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 RefreshToken 刷新 AccessToken, 返回刷新后的 Token.
|
||||
*
|
||||
* @return 刷新后的 Token
|
||||
*/
|
||||
public OAuth2Token getRefreshToken() {
|
||||
StorageSourceConfig refreshStorageSourceConfig =
|
||||
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.REFRESH_TOKEN_KEY);
|
||||
|
||||
String paramStr = "client_id=" + param.getClientId() +
|
||||
"&client_secret=" + param.getClientSecret() +
|
||||
"&refresh_token=" + refreshStorageSourceConfig.getValue() +
|
||||
"&grant_type=refresh_token" +
|
||||
"&access_type=offline";
|
||||
|
||||
log.info("{} 尝试刷新令牌, 参数信息为: {}", this, param);
|
||||
|
||||
HttpRequest post = HttpUtil.createPost(REFRESH_TOKEN_URL);
|
||||
|
||||
post.body(paramStr);
|
||||
HttpResponse response = post.execute();
|
||||
String body = response.body();
|
||||
log.info("{} 尝试刷新令牌成功, 响应信息为: {}", this, body);
|
||||
|
||||
JSONObject jsonBody = JSONObject.parseObject(body);
|
||||
|
||||
if (response.getStatus() != HttpStatus.OK.value()) {
|
||||
return OAuth2Token.fail(param.getClientId(), param.getClientSecret(), param.getRedirectUri(), body);
|
||||
}
|
||||
|
||||
String accessToken = jsonBody.getString("access_token");
|
||||
return OAuth2Token.success(param.getClientId(), param.getClientSecret(), param.getRedirectUri(), accessToken, null, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前存储源 AccessToken
|
||||
*/
|
||||
@Override
|
||||
public void refreshAccessToken() {
|
||||
try {
|
||||
OAuth2Token refreshToken = getRefreshToken();
|
||||
|
||||
if (refreshToken.getAccessToken() == null) {
|
||||
throw new StorageSourceRefreshTokenException("获取 AccessToken 失败, 获取到的令牌为空, 相关诊断信息为: " + refreshToken, storageId);
|
||||
}
|
||||
|
||||
StorageSourceConfig accessTokenConfig =
|
||||
storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY);
|
||||
accessTokenConfig.setValue(refreshToken.getAccessToken());
|
||||
|
||||
storageSourceConfigService.updateStorageConfig(Collections.singletonList(accessTokenConfig));
|
||||
RefreshTokenCache.putRefreshTokenInfo(storageId, RefreshTokenCache.RefreshTokenInfo.success());
|
||||
param.setAccessToken(refreshToken.getAccessToken());
|
||||
param.setRefreshToken(refreshToken.getRefreshToken());
|
||||
log.info("存储源 {} 刷新 AccessToken 成功", storageId);
|
||||
} catch (Exception e) {
|
||||
RefreshTokenCache.putRefreshTokenInfo(storageId, RefreshTokenCache.RefreshTokenInfo.fail(getStorageTypeEnum().getDescription() + " AccessToken 刷新失败: " + e.getMessage()));
|
||||
throw new StorageSourceRefreshTokenException("存储源 ID: [{}] 刷新 AccessToken 失败", e, storageId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 api 返回的 json array 为 zfile 文件对象列表
|
||||
*
|
||||
* @param jsonArray
|
||||
* api 返回文件 json array
|
||||
*
|
||||
* @param folderPath
|
||||
* 所属文件夹路径
|
||||
*
|
||||
* @return zfile 文件对象列表
|
||||
*/
|
||||
public List<FileItemResult> jsonArrayToFileList(JSONArray jsonArray, String folderPath) {
|
||||
ArrayList<FileItemResult> fileList = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
fileList.add(jsonObjectToFileItem(jsonArray.getJSONObject(i), folderPath));
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换 api 返回的 json object 为 zfile 文件对象
|
||||
*
|
||||
* @param jsonObject
|
||||
* api 返回文件 json object
|
||||
*
|
||||
* @param folderPath
|
||||
* 所属文件夹路径
|
||||
*
|
||||
* @return zfile 文件对象
|
||||
*/
|
||||
public FileItemResult jsonObjectToFileItem(JSONObject jsonObject, String folderPath) {
|
||||
FileItemResult fileItemResult = new FileItemResult();
|
||||
fileItemResult.setName(jsonObject.getString("name"));
|
||||
fileItemResult.setPath(folderPath);
|
||||
fileItemResult.setSize(jsonObject.getLong("size"));
|
||||
|
||||
String mimeType = jsonObject.getString("mimeType");
|
||||
if (ObjectUtil.equals(SHORTCUT_MIME_TYPE, mimeType)) {
|
||||
JSONObject shortcutDetails = jsonObject.getJSONObject("shortcutDetails");
|
||||
mimeType = shortcutDetails.getString("targetMimeType");
|
||||
}
|
||||
|
||||
if (StrUtil.equals(mimeType, FOLDER_MIME_TYPE)) {
|
||||
fileItemResult.setType(FileTypeEnum.FOLDER);
|
||||
} else {
|
||||
fileItemResult.setType(FileTypeEnum.FILE);
|
||||
fileItemResult.setUrl(getDownloadUrl(StringUtils.concat(folderPath, fileItemResult.getName())));
|
||||
}
|
||||
|
||||
fileItemResult.setTime(jsonObject.getDate("modifiedTime"));
|
||||
|
||||
if (fileItemResult.getSize() == null) {
|
||||
fileItemResult.setSize(-1L);
|
||||
}
|
||||
|
||||
return fileItemResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 请求参数类
|
||||
*/
|
||||
@Data
|
||||
class GoogleDriveAPIParam {
|
||||
|
||||
private final Integer DEFAULT_PAGE_SIZE = 1000;
|
||||
|
||||
// 存储源 id
|
||||
private String driveId;
|
||||
|
||||
// 是否返回共享驱动器或团队盘的内容
|
||||
private boolean includeItemsFromAllDrives;
|
||||
|
||||
// 查询适用的文件分组, 支持 'user', 'drive', 'allDrives'
|
||||
private String corpora;
|
||||
|
||||
// 请求的应用程序是否同时支持“我的云端硬盘”和共享云端硬盘
|
||||
private boolean supportsAllDrives;
|
||||
|
||||
// 请求的字段
|
||||
private String fields;
|
||||
|
||||
// 查询参数
|
||||
private String q;
|
||||
|
||||
// 每页多少条
|
||||
private Integer pageSize;
|
||||
|
||||
// 下页的页码
|
||||
private String pageToken;
|
||||
|
||||
/**
|
||||
* 根据路径获取 id 的 api 请求参数
|
||||
*
|
||||
* @param folderPath
|
||||
* 文件夹路径
|
||||
*/
|
||||
public String getDriveIdByPathParam(String folderPath, String parentId) {
|
||||
GoogleDriveAPIParam googleDriveAPIParam = getBasicParam();
|
||||
|
||||
String parentIdParam = "";
|
||||
|
||||
if (StrUtil.isNotEmpty(parentId)) {
|
||||
parentIdParam = "'" + parentId + "' in parents and ";
|
||||
}
|
||||
|
||||
googleDriveAPIParam.setFields("files(id)");
|
||||
googleDriveAPIParam.setQ(parentIdParam + " name = '" + folderPath + "' and trashed = false");
|
||||
|
||||
return googleDriveAPIParam.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据路径获取 id 的 api 请求参数
|
||||
*
|
||||
* @param folderId
|
||||
* google drive 文件夹 id
|
||||
*
|
||||
* @param pageToken
|
||||
* 分页 token
|
||||
*/
|
||||
public String getFileListParam(String folderId, String pageToken) {
|
||||
GoogleDriveAPIParam googleDriveAPIParam = getBasicParam();
|
||||
|
||||
googleDriveAPIParam.setFields("files(id,name,mimeType,shortcutDetails,size,modifiedTime),nextPageToken");
|
||||
googleDriveAPIParam.setQ("'" + folderId + "' in parents and trashed = false");
|
||||
googleDriveAPIParam.setPageToken(pageToken);
|
||||
googleDriveAPIParam.setPageSize(DEFAULT_PAGE_SIZE);
|
||||
return googleDriveAPIParam.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据关键字和路径搜索文件 api 请求参数
|
||||
*
|
||||
* @param folderId
|
||||
* 搜索的父文件夹 id
|
||||
*
|
||||
* @param pageToken
|
||||
* 分页 token
|
||||
*
|
||||
* @param keyword
|
||||
* 搜索关键字
|
||||
*/
|
||||
public String getSearchParam(String folderId, String pageToken, String keyword) {
|
||||
GoogleDriveAPIParam googleDriveAPIParam = getBasicParam();
|
||||
|
||||
String parentIdParam = "";
|
||||
if (StrUtil.isNotEmpty(folderId)) {
|
||||
parentIdParam = "'" + folderId + "' in parents and ";
|
||||
}
|
||||
|
||||
googleDriveAPIParam.setFields("files(id,name,mimeType,shortcutDetails,size,modifiedTime),nextPageToken");
|
||||
googleDriveAPIParam.setQ(parentIdParam + " name contains '" + keyword + "' and trashed = false");
|
||||
googleDriveAPIParam.setPageToken(pageToken);
|
||||
googleDriveAPIParam.setPageSize(DEFAULT_PAGE_SIZE);
|
||||
return googleDriveAPIParam.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断是否是团队盘,填充基础参数
|
||||
*/
|
||||
public GoogleDriveAPIParam getBasicParam() {
|
||||
GoogleDriveAPIParam googleDriveAPIParam = new GoogleDriveAPIParam();
|
||||
String driveId = param.getDriveId();
|
||||
|
||||
// 判断是否是团队盘,如果是,则需要添加团队盘的参数
|
||||
boolean isTeamDrive = StrUtil.isNotEmpty(driveId);
|
||||
|
||||
googleDriveAPIParam.setCorpora("user");
|
||||
if (isTeamDrive) {
|
||||
googleDriveAPIParam.setDriveId(driveId);
|
||||
googleDriveAPIParam.setIncludeItemsFromAllDrives(true);
|
||||
googleDriveAPIParam.setSupportsAllDrives(true);
|
||||
googleDriveAPIParam.setCorpora("drive");
|
||||
}
|
||||
|
||||
return googleDriveAPIParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求对象转 url param string
|
||||
*
|
||||
* @return url param string
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
Field[] fields = ReflectUtil.getFields(this.getClass());
|
||||
|
||||
StringBuilder param = new StringBuilder();
|
||||
|
||||
for (Field field : fields) {
|
||||
if (StrUtil.startWith(field.getName(), "this")) {
|
||||
continue;
|
||||
}
|
||||
Object fieldValue = ReflectUtil.getFieldValue(this, field);
|
||||
|
||||
if (ObjectUtil.isNotEmpty(fieldValue) && ObjectUtil.notEqual(fieldValue, false)) {
|
||||
param.append(field.getName()).append("=").append(fieldValue).append("&");
|
||||
}
|
||||
}
|
||||
|
||||
param.deleteCharAt(param.length() - 1);
|
||||
return param.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user