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 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 ""; } }