From 8a52961ccd0dec46889c17f9f0683b8498813979 Mon Sep 17 00:00:00 2001 From: smallxy <2569224983@qq.com> Date: Wed, 23 Apr 2025 21:51:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=99=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/cyanbukkit/AliyahPanBackup.java | 422 ++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 src/main/java/cn/cyanbukkit/AliyahPanBackup.java diff --git a/src/main/java/cn/cyanbukkit/AliyahPanBackup.java b/src/main/java/cn/cyanbukkit/AliyahPanBackup.java new file mode 100644 index 0000000..a172d52 --- /dev/null +++ b/src/main/java/cn/cyanbukkit/AliyahPanBackup.java @@ -0,0 +1,422 @@ +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) { + logger.info("开始上传到阿里云盘..."); + + // 1. 获取 ACCESS_TOKEN + ACCESS_TOKEN = getAccessToken(); + if (ACCESS_TOKEN == null) { + logger.error("获取 ACCESS_TOKEN 失败"); + System.exit(1); + } + + // 2. 获取 drive_id + DRIVE_ID = getDriveId(); + if (DRIVE_ID == null) { + logger.error("获取 drive_id 失败"); + System.exit(1); + } + + // 3. 创建备份文件 + String zipFilePath = createBackupZip(); + + // 4. 上传文件 + boolean uploadSuccess = uploadToAlipan(zipFilePath); + if (uploadSuccess) { + logger.info("备份文件已成功上传到阿里云盘目录: {}", TARGET_FOLDER); + } else { + logger.error("上传到阿里云盘失败"); + System.exit(1); + } + } + + 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 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; + } + } + + // 标记文件上传完成 + 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 ""; + } +}