LT_Backup_AliyunDriver/src/main/java/cn/cyanbukkit/AliyahPanBackup.java

421 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cn.cyanbukkit;
import okhttp3.*;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class AliyahPanBackup {
private static final Logger logger = LoggerFactory.getLogger(AliyahPanBackup.class);
// 阿里云盘 API 配置
private static final String CLIENT_ID = "8a9342f6826a49508dc7911ca70c4d86";
private static final String CLIENT_SECRET = "0873fada20744a6e849c830d95cc7ee9";
private static final String REDIRECT_URI = "http://mc.lanternmc.cn";
private static final String AUTH_URL = "https://openapi.alipan.com/oauth/authorize";
private static final String TOKEN_URL = "https://openapi.alipan.com/oauth/access_token";
private static final String API_URL = "https://openapi.alipan.com";
private static final String TARGET_FOLDER = "lanternmc"; // 目标目录名称
private static String ACCESS_TOKEN;
private static String DRIVE_ID;
public static void main(String[] args) {
// 1. 创建备份文件
String zipFilePath = createBackupZip();
logger.info("开始上传到阿里云盘...");
// 2. 获取 ACCESS_TOKEN
ACCESS_TOKEN = getAccessToken();
if (ACCESS_TOKEN == null) {
logger.error("获取 ACCESS_TOKEN 失败");
System.exit(1);
}
// 3. 获取 drive_id
DRIVE_ID = getDriveId();
if (DRIVE_ID == null) {
logger.error("获取 drive_id 失败");
System.exit(1);
}
// 4.假设 uploadToAlipan 方法返回一个布尔值,表示上传是否成功
boolean uploadSuccess = false;
while (!uploadSuccess) {
uploadSuccess = uploadToAlipan(zipFilePath);
if (uploadSuccess) {
logger.info("备份文件已成功上传到阿里云盘目录: {}", TARGET_FOLDER);
} else {
logger.error("上传到阿里云盘失败,重试上传");
}
}
}
private static String getAccessToken() {
String scope = "file:all:read,file:all:write,user:base";
String authLink = AUTH_URL + "?client_id=" + CLIENT_ID + "&auto_login=true&redirect_uri=" + REDIRECT_URI + "&response_type=code&scope=" + scope;
logger.info("请按以下步骤操作:");
logger.info("1. 访问链接 {} 授权", authLink);
logger.info("2. 复制 LanternMC 的特有的 code");
// 生成二维码并直接渲染到终端
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
// 使用更小的尺寸(100x100)和更简单的字符(■和空格)
BitMatrix bitMatrix = qrCodeWriter.encode(authLink, BarcodeFormat.QR_CODE, 100, 100);
// 使用紧凑ASCII字符在终端显示二维码(每两行合并为一行)
for (int y = 0; y < bitMatrix.getHeight(); y += 2) {
StringBuilder sb = new StringBuilder();
for (int x = 0; x < bitMatrix.getWidth(); x++) {
boolean upper = bitMatrix.get(x, y);
boolean lower = y + 1 < bitMatrix.getHeight() && bitMatrix.get(x, y + 1);
if (upper && lower) sb.append("");
else if (upper) sb.append("");
else if (lower) sb.append("");
else sb.append(" ");
}
System.out.println(sb.toString());
}
logger.info("二维码已显示在终端");
} catch (WriterException e) {
logger.error("生成二维码失败", e);
}
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入获取到的 code: ");
String authCode;
try {
authCode = reader.readLine();
} catch (IOException e) {
logger.error("读取 code 失败", e);
return null;
}
if (authCode.isEmpty()) {
logger.error("错误:未提供 code");
return null;
}
// 请求 ACCESS_TOKEN
JSONObject tokenRequest = new JSONObject();
tokenRequest.put("client_id", CLIENT_ID);
tokenRequest.put("client_secret", CLIENT_SECRET);
tokenRequest.put("grant_type", "authorization_code");
tokenRequest.put("code", authCode);
String tokenResponse = sendPostRequest(TOKEN_URL, tokenRequest.toString(), "application/json");
try {
JSONObject jsonResponse = new JSONObject(tokenResponse);
String accessToken = jsonResponse.getString("access_token");
logger.info("授权成功!已获取 ACCESS_TOKEN");
return accessToken;
} catch (JSONException e) {
logger.error("获取 ACCESS_TOKEN 失败,响应: {}", tokenResponse);
logger.error("请检查授权码是否正确并重试");
return null;
}
}
private static String getDriveId() {
String driveInfoUrl = API_URL + "/adrive/v1.0/user/getDriveInfo";
String driveResponse = sendPostRequest(driveInfoUrl, "", "application/json", ACCESS_TOKEN);
try {
JSONObject jsonResponse = new JSONObject(driveResponse);
String backupDriveId = jsonResponse.optString("backup_drive_id");
if (!backupDriveId.isEmpty()) {
return backupDriveId;
} else {
String defaultDriveId = jsonResponse.optString("default_drive_id");
if (!defaultDriveId.isEmpty()) {
logger.warn("未获取到 backup_drive_id将使用 default_drive_id");
return defaultDriveId;
} else {
return null;
}
}
} catch (JSONException e) {
logger.error("获取 drive_id 失败,响应: {}", driveResponse);
return null;
}
}
private static String createBackupZip() {
String zipFileName = "backup_" + System.currentTimeMillis() + ".zip";
File zipFile = new File(zipFileName);
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
// 只备份特定目录,避免系统文件
List<String> backupDirs = List.of(
"/lanterntea",
"/opt/mcsmanager/daemon/data/InstanceConfig",
"/opt/mcsmanager/web/logs"
);
for (String dirPath : backupDirs) {
File dir = new File(dirPath);
if (dir.exists() && dir.isDirectory()) {
String prefix = "";
if (dirPath.equals("/lanterntea")) {
prefix = "服务端/";
} else if (dirPath.equals("/opt/mcsmanager/daemon/data/InstanceConfig")) {
prefix = "MCSM/";
} else if (dirPath.equals("/opt/mcsmanager/web/logs")) {
prefix = "日志/";
}
addDirectoryToZip(zos, dir, prefix);
} else {
logger.warn("{} 目录不存在", dirPath);
}
}
logger.info("备份完成ZIP 文件已保存为: {}", zipFileName);
return zipFileName;
} catch (IOException e) {
logger.error("压缩失败", e);
return null;
}
}
private static void addDirectoryToZip(ZipOutputStream zos, File dir, String parentDir) throws IOException {
for (File file : Objects.requireNonNull(dir.listFiles())) {
if (file.isDirectory()) {
// 跳过备份目录自身
if (!file.getAbsolutePath().equals(dir.getAbsolutePath())) {
logger.info("正在压缩目录: {}", parentDir + file.getName() + "/");
addDirectoryToZip(zos, file, parentDir + file.getName() + "/");
}
} else {
// 跳过已存在的备份文件
if (!file.getName().endsWith(".zip")) {
logger.info("正在压缩文件: {}", parentDir + file.getName());
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry zipEntry = new ZipEntry(parentDir + file.getName());
zos.putNextEntry(zipEntry);
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
}
}
}
}
}
private static boolean uploadToAlipan(String zipFilePath) {
File file = new File(zipFilePath);
String fileName = file.getName();
// 创建上传任务
String createUrl = API_URL + "/adrive/v1.0/openFile/create";
JSONObject createJson = new JSONObject();
createJson.put("drive_id", DRIVE_ID);
createJson.put("parent_file_id", "root");
createJson.put("name", fileName);
createJson.put("type", "file");
createJson.put("check_name_mode", "refuse");
// 计算分片信息
JSONArray partInfoList = new JSONArray();
long fileSize = file.length();
long minPartSize = 100 * 1024; // 100KB最小分片
long maxPartSize = 5L * 1024 * 1024 * 1024; // 5GB最大分片
long partSize = Math.min(Math.max(fileSize / 10000, minPartSize), maxPartSize); // 自动计算分片大小
int partCount = (int) Math.ceil((double) fileSize / partSize);
if (partCount > 10000) {
partCount = 10000;
partSize = (long) Math.ceil((double) fileSize / partCount);
}
for (int i = 1; i <= partCount; i++) {
JSONObject partInfo = new JSONObject();
partInfo.put("part_number", i);
partInfo.put("part_size", i == partCount ? fileSize - ((i - 1) * partSize) : partSize);
partInfoList.put(partInfo);
}
createJson.put("part_info_list", partInfoList);
createJson.put("size", file.length());
// 发送创建上传任务请求
String createResponse = sendPostRequest(createUrl, createJson.toString(), "application/json", ACCESS_TOKEN);
try {
JSONObject jsonResponse = new JSONObject(createResponse);
if (!jsonResponse.has("file_id") || !jsonResponse.has("upload_id")) {
logger.error("创建上传任务失败,响应缺少必要字段: {}", createResponse);
return false;
}
String fileId = jsonResponse.getString("file_id");
String uploadId = jsonResponse.getString("upload_id");
boolean rapidUpload = jsonResponse.optBoolean("rapid_upload");
if (rapidUpload) {
System.out.println("文件秒传成功!");
return true;
}
// 获取上传地址
String getUploadUrl = API_URL + "/adrive/v1.0/openFile/getUploadUrl";
JSONObject uploadParams = new JSONObject();
uploadParams.put("drive_id", DRIVE_ID);
uploadParams.put("file_id", fileId);
uploadParams.put("upload_id", uploadId);
uploadParams.put("part_info_list", partInfoList);
String uploadUrlResponse = sendPostRequest(getUploadUrl, uploadParams.toString(), "application/json", ACCESS_TOKEN);
JSONObject uploadUrlJson = new JSONObject(uploadUrlResponse);
if (!uploadUrlJson.has("part_info_list")) {
logger.error("获取上传地址失败,响应缺少必要字段: {}", uploadUrlResponse);
return false;
}
JSONArray receivedPartInfoList = uploadUrlJson.getJSONArray("part_info_list");
// 上传文件分片
for (int i = 0; i < receivedPartInfoList.length(); i++) {
JSONObject partInfo = receivedPartInfoList.getJSONObject(i);
String uploadUrl = partInfo.getString("upload_url");
int partNumber = partInfo.getInt("part_number");
// 处理URL中的特殊字符
if (uploadUrl.contains(" ")) {
uploadUrl = uploadUrl.replace(" ", "%20");
}
// 计算分片位置和实际大小
long pos = (partNumber - 1) * partSize;
long actualSize = partNumber == partCount ? fileSize - pos : partSize;
byte[] partContent = new byte[(int) actualSize];
// 读取分片内容
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
randomAccessFile.seek(pos);
randomAccessFile.readFully(partContent, 0, (int) actualSize);
} catch (IOException e) {
logger.error("读取文件分片失败", e);
return false;
}
// 上传分片
RequestBody body = RequestBody.create(partContent, null); // 不指定MediaType
Request request = new Request.Builder()
.url(uploadUrl)
.put(body)
.removeHeader("Content-Type") // 移除Content-Type以避免签名问题
.build();
try (Response response = new OkHttpClient().newCall(request).execute()) {
if (!response.isSuccessful()) {
logger.error("上传分片失败partNumber: {}, 响应: {}", partNumber, response.body().string());
return false;
}
logger.info("上传分片成功,进度: {}/{}", partNumber, partCount);
} catch (IOException e) {
logger.error("上传分片失败", e);
return false;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 标记文件上传完成
String completeUrl = API_URL + "/adrive/v1.0/openFile/complete";
JSONObject completeJson = new JSONObject();
completeJson.put("drive_id", DRIVE_ID);
completeJson.put("file_id", fileId);
completeJson.put("upload_id", uploadId);
String completeResponse = sendPostRequest(completeUrl, completeJson.toString(), "application/json", ACCESS_TOKEN);
try {
JSONObject completeResponseJson = new JSONObject(completeResponse);
if (completeResponseJson.has("status") && "available".equals(completeResponseJson.getString("status")) ||
completeResponseJson.has("file_id")) {
logger.info("文件上传成功文件ID: {}", completeResponseJson.optString("file_id"));
return true;
}
logger.error("标记文件上传完毕失败,响应: {}", completeResponse);
return false;
} catch (JSONException e) {
logger.error("标记文件上传完毕失败,响应: {}", completeResponse);
return false;
}
} catch (JSONException e) {
logger.error("上传失败,响应: {}", createResponse);
return false;
}
}
private static String sendPostRequest(String url, String data, String contentType) {
return sendPostRequest(url, data, contentType, null);
}
private static String sendPostRequest(String url, String data, String contentType, String accessToken) {
int maxRetries = 3;
int retryDelay = 1000; // 1秒
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 使用 URI 来构建 URL避免 URL(String) 构造函数的弃用问题
URI uri = URI.create(url);
URL newUrl = uri.toURL();
HttpURLConnection conn = (HttpURLConnection) newUrl.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", contentType);
conn.setRequestProperty("Accept", "application/json");
if (accessToken != null) {
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
}
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(data.getBytes(StandardCharsets.UTF_8));
}
int responseCode = conn.getResponseCode();
if (responseCode == 400) {
// 记录400错误的详细日志
logger.error("请求失败(尝试 {}/{}): Server returned HTTP response code: {} for URL: {}", attempt, maxRetries, responseCode, url);
logger.error("响应头: {}", conn.getHeaderFields());
logger.error("响应体: {}", new String(conn.getErrorStream().readAllBytes(), StandardCharsets.UTF_8));
} else if (responseCode == 404) {
logger.error("服务器返回404错误: {}", url);
} else if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
return response.toString();
} else {
logger.error("请求失败(尝试 {}/{}): {}", attempt, maxRetries, conn.getResponseMessage());
}
if (attempt < maxRetries) {
try {
Thread.sleep(retryDelay);
retryDelay *= 2; // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return "";
}
}
} catch (IOException ex) {
logger.error("请求失败(尝试 {}/{}): {}", attempt, maxRetries, ex.getMessage());
}
}
return "";
}
}