Remove pom.xml and RedisClipSync.java files; update README.md to reflect new project structure and build instructions.

This commit is contained in:
liu
2026-01-31 23:14:34 +08:00
parent f02b6d9682
commit 78b9685ee1
8 changed files with 172 additions and 27 deletions

70
code/pom.xml Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.redisclipsync</groupId>
<artifactId>redis-clip-sync</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Redis Clipboard Sync</name>
<description>Redis-based bidirectional clipboard sync: Master broadcasts to slaves, slaves upload to master and save to files.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.9.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>RedisClipSync</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>RedisClipSync</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>redis-clip-sync</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,261 @@
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import java.awt.*;
import java.awt.datatransfer.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
public class RedisClipSync {
// 配置项
private static String ROLE;
private static String REDIS_HOST;
private static int REDIS_PORT;
private static String REDIS_PASS;
// 主机专用配置
private static String[] MASTER_TARGETS;
private static String MASTER_SAVE_DIR;
// 从机专用配置
private static String SLAVE_LISTEN_CHANNEL;
private static String SLAVE_ID; // 用于生成文件名,如 192.168.1.5.txt
// 公共上传通道
private static final String UPLOAD_CHANNEL = "global_clip_upload";
// 状态控制
private static String lastContent = "";
private static volatile boolean isRemoteUpdate = false; // 锁标记,防止回环
public static void main(String[] args) {
String configPath = args.length > 0 ? args[0] : (System.getProperty("config") != null ? System.getProperty("config") : "config.properties");
loadConfig(configPath);
createDirIfMaster();
if (!checkRedisConnection()) {
System.err.println("请检查 config 中 redis.host / redis.port / redis.password 是否与 Redis 服务器一致,且本机能否访问该 Redis。");
System.exit(1);
}
System.out.println(">>> JClipSync Pro 启动 <<<");
System.out.println("角色: " + ROLE);
if ("MASTER".equalsIgnoreCase(ROLE)) {
// 1. 监听本地 -> 广播给从机
startClipboardPoller("MASTER", RedisClipSync::publishToSlaves);
// 2. 监听上传通道 -> 写入文件
startMasterUploadListener();
} else {
// 1. 监听 Redis -> 写入本地
startSlaveDownloadListener();
// 2. 监听本地 -> 上传给主机 (存文件)
startClipboardPoller("SLAVE", RedisClipSync::uploadToMaster);
}
}
// ==========================================
// 公共模块: 剪切板轮询
// ==========================================
interface ClipCallback { void onCopy(String content); }
private static void startClipboardPoller(String sourceName, ClipCallback callback) {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
new Thread(() -> {
System.out.println("[" + sourceName + "] 剪切板监听已启动...");
while (true) {
try {
Thread.sleep(500);
// 如果刚被远程写入,则跳过本次检测,防止死循环
if (isRemoteUpdate) {
isRemoteUpdate = false;
// 更新本地缓存,认为当前内容是已知的
Transferable c = clipboard.getContents(null);
if (c != null && c.isDataFlavorSupported(DataFlavor.stringFlavor)) {
lastContent = (String) c.getTransferData(DataFlavor.stringFlavor);
}
continue;
}
Transferable contents = clipboard.getContents(null);
if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
String currentText = (String) contents.getTransferData(DataFlavor.stringFlavor);
if (!currentText.equals(lastContent)) {
lastContent = currentText;
callback.onCopy(currentText);
}
}
} catch (Exception e) { /* 忽略 */ }
}
}).start();
}
// ==========================================
// Master 逻辑
// ==========================================
// 动作:广播给所有从机
private static void publishToSlaves(String content) {
System.out.println("[Master] 本地复制,广播中...");
Jedis jedis = getJedis();
try {
for (String channel : MASTER_TARGETS) {
String ch = channel.trim();
if (!ch.isEmpty()) {
jedis.publish(ch, content);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) jedis.close();
}
}
// 动作:监听上传通道并存文件
private static void startMasterUploadListener() {
new Thread(() -> {
while (true) {
try (Jedis jedis = getJedis()) {
System.out.println("[Master] 等待从机上传数据...");
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// 消息格式约定: "SLAVE_ID|||CONTENT"
String[] parts = message.split("\\|\\|\\|", 2);
if (parts.length == 2) {
String slaveId = parts[0];
String content = parts[1];
saveToFile(slaveId, content);
}
}
}, UPLOAD_CHANNEL);
} catch (Exception e) {
System.err.println("[Master] Redis 断开,重连中... 原因: " + e.getMessage());
e.printStackTrace();
try { Thread.sleep(3000); } catch (InterruptedException ig) { Thread.currentThread().interrupt(); }
}
}
}).start();
}
// 动作:保存到文件
private static void saveToFile(String fileName, String content) {
try {
File dir = new File(MASTER_SAVE_DIR);
if (!dir.exists()) dir.mkdirs();
File file = new File(dir, fileName + ".txt");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String logEntry = String.format("[%s]\n%s\n\n------------------\n\n", sdf.format(new Date()), content);
Files.write(file.toPath(), logEntry.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
System.out.println("[Master] 已保存来自 [" + fileName + "] 的数据");
} catch (IOException e) {
System.err.println("文件写入失败: " + e.getMessage());
}
}
// ==========================================
// Slave 逻辑
// ==========================================
// 动作:上传到 Master
private static void uploadToMaster(String content) {
System.out.println("[Slave] 检测到复制,正在上传给主机...");
try (Jedis jedis = getJedis()) {
String payload = SLAVE_ID + "|||" + content;
jedis.publish(UPLOAD_CHANNEL, payload);
} catch (Exception e) {
System.err.println("上传失败: " + e.getMessage());
}
}
// 动作:接收主机指令
private static void startSlaveDownloadListener() {
new Thread(() -> {
while (true) {
try (Jedis jedis = getJedis()) {
System.out.println("[Slave] 监听主机同步...");
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("[Slave] 收到主机内容,写入剪切板");
writeToClipboard(message);
}
}, SLAVE_LISTEN_CHANNEL);
} catch (Exception e) {
try { Thread.sleep(3000); } catch (InterruptedException ig) { Thread.currentThread().interrupt(); }
}
}
}).start();
}
private static void writeToClipboard(String text) {
isRemoteUpdate = true; // 标记:这是远程来的,不要回传!
try {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(new StringSelection(text), null);
} catch (Exception e) {
isRemoteUpdate = false;
}
}
// ==========================================
// 辅助工具
// ==========================================
private static Jedis getJedis() {
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);
if (REDIS_PASS != null && !REDIS_PASS.isEmpty()) jedis.auth(REDIS_PASS);
return jedis;
}
/** 启动前检查 Redis 是否可连 */
private static boolean checkRedisConnection() {
try (Jedis jedis = getJedis()) {
jedis.ping();
System.out.println("[连接] Redis " + REDIS_HOST + ":" + REDIS_PORT + " 连接成功");
return true;
} catch (Exception e) {
System.err.println("[连接] Redis 连接失败: " + e.getMessage());
return false;
}
}
private static void createDirIfMaster() {
if ("MASTER".equalsIgnoreCase(ROLE)) {
File dir = new File(MASTER_SAVE_DIR);
if (!dir.exists()) dir.mkdirs();
}
}
private static void loadConfig(String configPath) {
try (FileInputStream fis = new FileInputStream(configPath)) {
Properties props = new Properties();
props.load(new InputStreamReader(fis, StandardCharsets.UTF_8));
ROLE = props.getProperty("role", "SLAVE");
REDIS_HOST = props.getProperty("redis.host", "127.0.0.1");
REDIS_PORT = Integer.parseInt(props.getProperty("redis.port", "6379"));
REDIS_PASS = props.getProperty("redis.password", "");
MASTER_TARGETS = props.getProperty("master.target.channels", "").split(",");
MASTER_SAVE_DIR = props.getProperty("master.save.dir", "./clipboard_logs/");
SLAVE_LISTEN_CHANNEL = props.getProperty("slave.listen.channel", "default");
SLAVE_ID = props.getProperty("slave.id", SLAVE_LISTEN_CHANNEL);
} catch (Exception e) {
System.err.println("配置加载失败,请检查 " + configPath + ": " + e.getMessage());
System.exit(1);
}
}
}