421 lines
19 KiB
Java
421 lines
19 KiB
Java
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 "";
|
||
}
|
||
}
|