diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 8b1a284..fd69015 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -40,8 +40,10 @@ declare module 'vue' { DialogTemp: typeof import('./src/components/window/DialogTemp.vue')['default'] EditFileName: typeof import('./src/components/builtin/EditFileName.vue')['default'] EditType: typeof import('./src/components/builtin/EditType.vue')['default'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElBadge: typeof import('element-plus/es')['ElBadge'] ElButton: typeof import('element-plus/es')['ElButton'] + ElCard: typeof import('element-plus/es')['ElCard'] ElCarousel: typeof import('element-plus/es')['ElCarousel'] ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] diff --git a/frontend/src/components/builtin/FileList.vue b/frontend/src/components/builtin/FileList.vue index c0a5a6e..db34348 100644 --- a/frontend/src/components/builtin/FileList.vue +++ b/frontend/src/components/builtin/FileList.vue @@ -124,15 +124,8 @@ function getName(item: any) { } } function handleOnOpen(item: OsFileWithoutContent) { - chosenIndexs.value = []; - if (choose.ifShow && !item.isDirectory) { - choose.path.push(item.path) - choose.close() - } else { props.onOpen(item); emitEvent('desktop.app.open'); - } - } function hadnleDrop(mouse: DragEvent, path: string) { hoverIndex.value = -1; @@ -325,22 +318,10 @@ function handleRightClick(mouse: MouseEvent, item: OsFileWithoutContent, index: } if (choose.ifShow) { menuArr.push({ - label: t('selected'), + label: "选中发送", click: () => { - const paths: any = [] - chosenIndexs.value.forEach((index) => { - const item = props.fileList[index]; - if (!item.isDirectory) { - paths.push(item.path) - } - }) - if (paths.length > 0) { - choose.path = paths - choose.close() - } - chosenIndexs.value = []; - - + choose.path = item.path + choose.close() }, }) } diff --git a/frontend/src/components/desktop/LockDesktop.vue b/frontend/src/components/desktop/LockDesktop.vue index a9c74a3..7a4e49b 100644 --- a/frontend/src/components/desktop/LockDesktop.vue +++ b/frontend/src/components/desktop/LockDesktop.vue @@ -1,12 +1,104 @@ + + - + \ No newline at end of file diff --git a/frontend/src/components/localchat/Chat.vue b/frontend/src/components/localchat/Chat.vue index ef2eea1..49375af 100644 --- a/frontend/src/components/localchat/Chat.vue +++ b/frontend/src/components/localchat/Chat.vue @@ -13,59 +13,59 @@ diff --git a/frontend/src/components/taskbar/StartOption.vue b/frontend/src/components/taskbar/StartOption.vue index 02698eb..d7b218c 100644 --- a/frontend/src/components/taskbar/StartOption.vue +++ b/frontend/src/components/taskbar/StartOption.vue @@ -28,12 +28,14 @@ import { emitEvent } from "@/system/event"; import { useSystem, Dialog, t, Menu, BrowserWindow } from "@/system"; import { vGlowing } from "@/util/glowingBorder"; +import { fetchGet } from "@/system/config"; +import { RestartApp } from "@/util/goutil"; const sys = useSystem(); function handleClick(key: number, ev: MouseEvent) { switch (key) { case 0: - Menu.buildFromTemplate([ + const actions = [ { label: t("startMenu.shutdown"), click: () => { @@ -55,7 +57,19 @@ function handleClick(key: number, ev: MouseEvent) { }); }, }, - ]).popup(ev); + ] + if(sys.getConfig('userType') == 'member'){ + actions.push({ + label: '退出', + click: () => { + const userInfo:any = sys.getConfig('userInfo') + fetchGet(userInfo.url + "/member/loginout").then(() => { + RestartApp() + }) + }, + }) + } + Menu.buildFromTemplate(actions).popup(ev); break; case 1: { diff --git a/frontend/src/stores/choose.ts b/frontend/src/stores/choose.ts index 81284ad..44d0a19 100644 --- a/frontend/src/stores/choose.ts +++ b/frontend/src/stores/choose.ts @@ -3,7 +3,7 @@ import { BrowserWindow } from "@/system"; import { ref } from 'vue'; export const useChooseStore = defineStore('chooseStore', () => { const win:any = ref() - const path:any = ref([]) + const path:any = ref("") const ifShow = ref(false) const select = (title = '选择文件', fileExt:any) => { win.value = new BrowserWindow({ diff --git a/frontend/src/stores/localchat.ts b/frontend/src/stores/localchat.ts index 5001bda..93b6ac5 100644 --- a/frontend/src/stores/localchat.ts +++ b/frontend/src/stores/localchat.ts @@ -27,6 +27,9 @@ export const useLocalChatStore = defineStore('localChatStore', () => { const chatTargetId = ref(0) const chatTargetIp = ref("") const showAddUser = ref(false) + const handlerMessage = (message : any) => { + console.log(message) + } const handleSelect = (key: number) => { navId.value = key; }; @@ -344,10 +347,15 @@ export const useLocalChatStore = defineStore('localChatStore', () => { const targetUser = userList.value.find((d: any) => d.ip === chatTargetIp.value) //console.log(targetUser) if (targetUser.isOnline) { - const postUrl = `http://${targetUser.ip}:56780/localchat/message` + const postUrl = `${config.apiUrl}/localchat/message` + const messages = { + type: 'text', + message: saveMsg.content, + ip: saveMsg.targetIp + } const completion = await fetch(postUrl, { method: "POST", - body: JSON.stringify(saveMsg), + body: JSON.stringify(messages), }) if (!completion.ok) { console.log(completion) @@ -507,7 +515,8 @@ export const useLocalChatStore = defineStore('localChatStore', () => { moreMsgList, refreshUserList, clearMsg, - addUser + addUser, + handlerMessage } }, { persist: { diff --git a/frontend/src/stores/upgrade.ts b/frontend/src/stores/upgrade.ts index 59d8811..0bc571a 100644 --- a/frontend/src/stores/upgrade.ts +++ b/frontend/src/stores/upgrade.ts @@ -4,6 +4,7 @@ import { getSystemKey, setSystemKey, parseJson, getSystemConfig } from '@/system import { RestartApp } from '@/util/goutil'; import { ElMessage } from 'element-plus' import { t } from '@/i18n'; +import { useLocalChatStore } from "./localchat"; export const useUpgradeStore = defineStore('upgradeStore', () => { const hasUpgrade = ref(false); const hasNotice = ref(false); @@ -15,6 +16,7 @@ export const useUpgradeStore = defineStore('upgradeStore', () => { const progress = ref(0) const noticeList:any = ref([]) const adList:any = ref([]) + const localChatStore = useLocalChatStore() function compareVersions(version1:string, version2:string) { // 将版本号字符串按"."分割成数组 const parts1 = version1.split('.').map(Number); @@ -53,6 +55,9 @@ export const useUpgradeStore = defineStore('upgradeStore', () => { case 'update': checkUpdate(message.data.data) break; + case 'localchat': + localChatStore.handlerMessage(message) + break; default: console.warn('Unknown message type:', message.type); } diff --git a/godo/cmd/main.go b/godo/cmd/main.go index a3b1171..ed44c3a 100644 --- a/godo/cmd/main.go +++ b/godo/cmd/main.go @@ -79,10 +79,10 @@ func OsStart() { fileRouter.HandleFunc("/watch", files.WatchHandler).Methods(http.MethodGet) localchatRouter := router.PathPrefix("/localchat").Subrouter() - localchatRouter.HandleFunc("/sse", localchat.SseHandler).Methods(http.MethodGet) + // localchatRouter.HandleFunc("/sse", localchat.SseHandler).Methods(http.MethodGet) localchatRouter.HandleFunc("/message", localchat.HandleMessage).Methods(http.MethodPost) - localchatRouter.HandleFunc("/upload", localchat.MultiUploadHandler).Methods(http.MethodPost) - localchatRouter.HandleFunc("/check", localchat.CheckUserHanlder).Methods(http.MethodGet) + // localchatRouter.HandleFunc("/upload", localchat.MultiUploadHandler).Methods(http.MethodPost) + // localchatRouter.HandleFunc("/check", localchat.CheckUserHanlder).Methods(http.MethodGet) // 注册 WebDAV 路由 webdavRouter := router.PathPrefix("/webdav").Subrouter() diff --git a/godo/go.mod b/godo/go.mod index 96f5a2b..75cbf11 100644 --- a/godo/go.mod +++ b/godo/go.mod @@ -8,6 +8,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/minio/selfupdate v0.6.0 github.com/shirou/gopsutil v3.21.11+incompatible + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 ) require ( diff --git a/godo/go.sum b/godo/go.sum index be86de1..11d6708 100644 --- a/godo/go.sum +++ b/godo/go.sum @@ -29,6 +29,7 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/godo/localchat/client.go b/godo/localchat/client.go deleted file mode 100644 index 380adcb..0000000 --- a/godo/localchat/client.go +++ /dev/null @@ -1,111 +0,0 @@ -package localchat - -import ( - "encoding/json" - "fmt" - "log" - "net" - "os" - "time" -) - -func DiscoverServers() { - broadcastTicker := time.NewTicker(broadcartTime) - done := make(chan struct{}) // New channel to signal when to stop - - go func() { - for { - select { - case <-broadcastTicker.C: - conn, err := net.Dial("udp", broadcastAddr) - if err != nil { - fmt.Println(err) - continue - } - - myIP, myHostname, err := getMyIPAndHostname() - if err != nil { - log.Printf("Failed to get my IP and hostname: %v", err) - conn.Close() - continue - } - - msg := UdpMessage{ - Type: "online", - Hostname: myHostname, - IP: myIP, - Message: time.Now().Format("2006-01-02 15:04:05"), - } - jsonData, err := json.Marshal(msg) - if err != nil { - log.Printf("Failed to marshal message to JSON: %v", err) - conn.Close() - continue - } - //log.Printf("Sending message: %+v", msg) - _, err = conn.Write(jsonData) - if err != nil { - fmt.Println(err) - } - conn.Close() // Close the connection after use - case <-done: // Signal to stop - broadcastTicker.Stop() - return - } - } - }() -} - -// 获取自己的IP地址和主机名 -func getMyIPAndHostname() (string, string, error) { - hostname, err := os.Hostname() - if err != nil { - return "", "", fmt.Errorf("failed to get hostname: %w", err) - } - - addrs, err := net.Interfaces() - if err != nil { - return "", "", fmt.Errorf("failed to get network interfaces: %w", err) - } - - var preferredIP net.IP - for _, iface := range addrs { - if iface.Flags&net.FlagUp == 0 { - // Skip interfaces that are not up - continue - } - ifAddrs, err := iface.Addrs() - if err != nil { - continue // Ignore this interface if we can't get its addresses - } - for _, addr := range ifAddrs { - var ip net.IP - switch v := addr.(type) { - case *net.IPNet: - ip = v.IP - case *net.IPAddr: - ip = v.IP - default: - continue - } - if ip.IsLoopback() { - continue // Skip loopback addresses - } - if ip.To4() != nil && (ip.IsPrivate() || ip.IsGlobalUnicast()) { - // Prefer global unicast or private addresses over link-local - preferredIP = ip - break - } - } - if preferredIP != nil { - // Found a preferred IP, break out of the loop - break - } - } - - if preferredIP == nil { - return "", "", fmt.Errorf("no preferred non-loopback IPv4 address found") - } - - return preferredIP.String(), hostname, nil -} diff --git a/godo/localchat/file.go b/godo/localchat/file.go index 2c4b8b9..2adaf03 100644 --- a/godo/localchat/file.go +++ b/godo/localchat/file.go @@ -1,20 +1,206 @@ package localchat import ( + "encoding/json" + "fmt" "godo/libs" + "io" + "log" + "net" + "net/http" "os" "path/filepath" "time" ) -func GetChatPath() (string, error) { +const ( + fileSize = 1024 // 每个数据包的大小 +) + +type FileChunk struct { + ChunkIndex int `json:"chunk_index"` + Data []byte `json:"data"` + Checksum uint32 `json:"checksum"` + Timestamp time.Time `json:"timestamp"` + Filename string `json:"filename"` +} + +func FileHandler(w http.ResponseWriter, r *http.Request) { + // 初始化多播地址 + var msg UdpMessage + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&msg); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + toIp := msg.IP + preferredIP, err := GetMyIp() + if err != nil { + http.Error(w, "Failed to get preferred IP", http.StatusInternalServerError) + return + } + msg.IP = preferredIP + msg.Type = "file" + basePath, err := libs.GetOsDir() + if err != nil { + libs.HTTPError(w, http.StatusInternalServerError, err.Error()) + return + } + filePath := filepath.Join(basePath, msg.Message.(string)) + // 处理单个文件或整个文件夹 + if fileInfo, err := os.Stat(filePath); err == nil { + if fileInfo.IsDir() { + handleDirectory(filePath, toIp, msg) + } else { + handleFile(filePath, toIp, msg) + } + } else { + http.Error(w, "Failed to stat path", http.StatusInternalServerError) + return + } +} + +func handleFile(filePath string, toIp string, message UdpMessage) { + // 打开文件 + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("Failed to open file: %v", err) + } + defer file.Close() + + // 获取文件大小 + fileInfo, err := file.Stat() + if err != nil { + log.Fatalf("Failed to get file info: %v", err) + } + fileSize := fileInfo.Size() + + // 计算需要发送的数据包数量 + numChunks := (fileSize + fileSize - 1) / fileSize + + // 发送文件 + SendFile(file, int(numChunks), toIp, message) +} + +func handleDirectory(dirPath string, toIp string, message UdpMessage) { + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + handleFile(path, toIp, message) + } + return nil + }) + if err != nil { + log.Fatalf("Failed to walk directory: %v", err) + } +} +func SendFile(file *os.File, numChunks int, toIp string, message UdpMessage) { + // 逐块读取文件并发送 + for i := 0; i < numChunks; i++ { + var chunkData [fileSize]byte + n, err := file.Read(chunkData[:]) + if err != nil && err != io.EOF { + log.Fatalf("Failed to read file chunk: %v", err) + } + + // 创建文件块 + chunk := FileChunk{ + ChunkIndex: i, + Data: chunkData[:n], + Checksum: calculateChecksum(chunkData[:n]), + Timestamp: time.Now(), + Filename: filepath.Base(file.Name()), + } + message.Message = chunk + // 将文件块转换为 JSON 格式 + data, err := json.Marshal(message) + if err != nil { + log.Fatalf("Failed to marshal chunk: %v", err) + } + + // 发送文件块 + addr, err := net.ResolveUDPAddr("udp4", toIp) + if err != nil { + log.Fatalf("Failed to resolve UDP address: %v", err) + } + + conn, err := net.DialUDP("udp4", nil, addr) + if err != nil { + log.Fatalf("Failed to dial UDP address: %v", err) + } + defer conn.Close() + + _, err = conn.Write(data) + if err != nil { + log.Printf("Failed to write data: %v", err) + } + + fmt.Printf("发送文件块 %d 到 %s 成功\n", i, toIp) + } +} +func RecieveFile(msg UdpMessage) { + chunk := msg.Message.(FileChunk) + + // 验证校验和 + calculatedChecksum := calculateChecksum(chunk.Data) + if calculatedChecksum != chunk.Checksum { + fmt.Printf("Checksum mismatch for chunk %d from %s\n", chunk.ChunkIndex, msg.IP) + return + } + baseDir, err := libs.GetOsDir() if err != nil { - return "", err + log.Printf("Failed to get OS directory: %v", err) + return + } + + // 创建接收文件的目录 + receiveDir := filepath.Join(baseDir, "C", "Users", "Reciv", time.Now().Format("2006-01-02")) + if !libs.PathExists(receiveDir) { + err := os.MkdirAll(receiveDir, 0755) + if err != nil { + log.Printf("Failed to create receive directory: %v", err) + return + } } - modelDir := filepath.Join(baseDir, "C", "Users", "Reciv", time.Now().Format("2006-01-02")) - if !libs.PathExists(modelDir) { - os.MkdirAll(modelDir, 0755) + + // 确定文件路径 + filePath := filepath.Join(receiveDir, chunk.Filename) + + // 如果文件不存在,则创建新文件 + if _, err := os.Stat(filePath); os.IsNotExist(err) { + file, err := os.Create(filePath) + if err != nil { + log.Printf("Failed to create file: %v", err) + return + } + defer file.Close() + } + + // 打开或追加到现有文件 + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + log.Printf("Failed to open file: %v", err) + return + } + defer file.Close() + + // 写入数据 + _, err = file.Write(chunk.Data) + if err != nil { + log.Printf("Failed to write data to file: %v", err) + return + } + + fmt.Printf("接收到文件块 %d 从 %s 成功\n", chunk.ChunkIndex, msg.IP) +} +func calculateChecksum(data []byte) uint32 { + checksum := uint32(0) + for _, b := range data { + checksum += uint32(b) } - return modelDir, nil + return checksum } diff --git a/godo/localchat/info.go b/godo/localchat/info.go new file mode 100644 index 0000000..29bb6d6 --- /dev/null +++ b/godo/localchat/info.go @@ -0,0 +1,67 @@ +package localchat + +import ( + "fmt" + "net" + "os" +) + +// 获取自己的IP地址和主机名 +func GetMyIPAndHostname() (string, string, error) { + hostname, err := os.Hostname() + if err != nil { + return "", "", fmt.Errorf("failed to get hostname: %w", err) + } + preferredIP, err := GetMyIp() + if err != nil { + return "", "", fmt.Errorf("failed to get IP address: %w", err) + } + + return preferredIP, hostname, nil +} +func GetMyIp() (string, error) { + addrs, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("failed to get network interfaces: %w", err) + } + + var preferredIP net.IP + for _, iface := range addrs { + if iface.Flags&net.FlagUp == 0 { + // Skip interfaces that are not up + continue + } + ifAddrs, err := iface.Addrs() + if err != nil { + continue // Ignore this interface if we can't get its addresses + } + for _, addr := range ifAddrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + default: + continue + } + if ip.IsLoopback() { + continue // Skip loopback addresses + } + if ip.To4() != nil && (ip.IsPrivate() || ip.IsGlobalUnicast()) { + // Prefer global unicast or private addresses over link-local + preferredIP = ip + break + } + } + if preferredIP != nil { + // Found a preferred IP, break out of the loop + break + } + } + + if preferredIP == nil { + return "", fmt.Errorf("no preferred non-loopback IPv4 address found") + } + return preferredIP.String(), nil +} diff --git a/godo/localchat/server.go b/godo/localchat/server.go deleted file mode 100644 index 5e9cc33..0000000 --- a/godo/localchat/server.go +++ /dev/null @@ -1,44 +0,0 @@ -package localchat - -import ( - "encoding/json" - "fmt" - "log" - "net" -) - -func StartServiceDiscovery() { - // 解析多播地址 - addr, err := net.ResolveUDPAddr("udp4", broadcastAddr) - if err != nil { - fmt.Println("Error resolving multicast address:", err) - return - } - - // 监听本地网络接口上的多播地址 - conn, err := net.ListenMulticastUDP("udp4", nil, addr) - if err != nil { - fmt.Println("Error listening on multicast address:", err) - return - } - defer conn.Close() - - buffer := make([]byte, 1024) - for { - n, addr, err := conn.ReadFromUDP(buffer) - if err != nil { - log.Printf("Error reading from UDP: %v,addr:%v", err, addr) - continue - } - //fmt.Printf("Received message: %s from %s\n", buffer[:n], addr) - - var udpMsg UdpMessage - err = json.Unmarshal(buffer[:n], &udpMsg) - if err != nil { - fmt.Printf("Error unmarshalling JSON: %v\n", err) - continue - } - //log.Printf("Get message: %+v", udpMsg) - OnlineUsers[udpMsg.IP] = udpMsg - } -} diff --git a/godo/localchat/sse.go b/godo/localchat/sse.go deleted file mode 100644 index bf00a66..0000000 --- a/godo/localchat/sse.go +++ /dev/null @@ -1,133 +0,0 @@ -package localchat - -import ( - "encoding/json" - "fmt" - "godo/libs" - "log" - "net/http" - "os" - "time" -) - -func init() { - go StartServiceDiscovery() - go DiscoverServers() -} - -func SseHandler(w http.ResponseWriter, r *http.Request) { - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.WriteHeader(http.StatusOK) - ticker := time.NewTicker(broadcartTime) // 每3秒检查一次在线用户 - defer ticker.Stop() - // 处理新消息 - ctx := r.Context() - // 使用Context来监听请求的取消 - go func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered in SSE goroutine: %v", r) - } - }() - - for { - select { - case <-ctx.Done(): // 当请求被取消时,退出循环 - return - case msg := <-messageChan: - // 构造JSON数据 - jsonData, err := json.Marshal(msg) - if err != nil { - log.Printf("Failed to marshal message to JSON: %v", err) - continue - } - - // 通过SSE发送JSON数据 - fmt.Fprintf(w, "data: %s\n\n", string(jsonData)) - flusher.Flush() - } - } - }() - myIP, myHostname, err := getMyIPAndHostname() - if err != nil { - log.Printf("Failed to get my IP and hostname: %v", err) - return - } - for { - select { - case <-ticker.C: // 每隔一段时间检查并广播在线用户 - var userList []UdpMessage - // 首先将自己的IP和主机名放入列表 - myMsg := UdpMessage{ - IP: myIP, - Hostname: myHostname, - Type: "online", - Message: time.Now().Format("2006-01-02 15:04:05"), - } - userList = append(userList, myMsg) - //log.Printf("Online users: %v", OnlineUsers) - for ip, info := range OnlineUsers { - if ip != myIP { // 确保不重复添加自己 - userList = append(userList, info) - } - } - res := UserList{Type: "user_list", Content: userList} - // 将用户列表转换为JSON字符串 - jsonData, err := json.Marshal(res) - if err != nil { - log.Printf("Failed to marshal online users to JSON: %v", err) - continue - } - // 通过SSE发送JSON数据 - fmt.Fprintf(w, "data: %s\n\n", string(jsonData)) - flusher.Flush() - } - } -} -func HandleMessage(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - var msg Message - decoder := json.NewDecoder(r.Body) - if err := decoder.Decode(&msg); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - // 将消息放入messageChan - messageChan <- msg - //log.Printf("Received text message from %s: %s", msg.SenderInfo.IP, msg.Content) - // 这里可以添加存储文本消息到数据库或其他处理逻辑 - w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, "Text message send successfully") -} -func CheckUserHanlder(w http.ResponseWriter, r *http.Request) { - res := map[string]any{} - res["code"] = 0 - res["message"] = "ok" - // 获取主机名 - hostname, err := os.Hostname() - if err == nil { - hostname = "Unknown" - } - ip, _ := libs.GetIPAddress() - - res["data"] = map[string]any{ - "ip": ip, - "hostname": hostname, - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(res) -} diff --git a/godo/localchat/type.go b/godo/localchat/type.go deleted file mode 100644 index d50e0b6..0000000 --- a/godo/localchat/type.go +++ /dev/null @@ -1,63 +0,0 @@ -package localchat - -import ( - "sync" - "time" -) - -// type SenderInfo struct { -// SenderIP string `json:"sender_ip"` -// Username string `json:"username"` -// } -type Message struct { - Type string `json:"type"` // 消息类型,如"text"、"image"等 - Content string `json:"content"` // 消息内容 - SenderInfo UserInfo `json:"senderInfo"` // 发送者的IP地址 - FileInfo FilePartInfo `json:"fileInfo"` - FileList []UploadInfo `json:"fileList"` -} -type UserInfo struct { - IP string `json:"ip"` - Hostname string `json:"hostname"` -} -type UdpMessage struct { - Type string `json:"type"` - IP string `json:"ip"` - Hostname string `json:"hostname"` - Message string `json:"message"` -} -type UserList struct { - Type string `json:"type"` - Content []UdpMessage `json:"content"` -} -type UploadInfo struct { - Name string `json:"name"` - SavePath string `json:"save_path"` - Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` -} - -// 文件分片信息 -type FilePartInfo struct { - FileName string `json:"filename"` - PartNumber int `json:"part_number"` - TotalParts int `json:"total_parts"` -} - -// 分片上传状态跟踪 -type UploadStatus struct { - sync.Mutex - Status map[string]int // key: fileName, value: 已上传分片数 -} - -var ( - messageChan = make(chan Message, 100) // 缓存大小根据实际情况设定 -) -var uploadStatus = UploadStatus{Status: make(map[string]int)} -var broadcartTime = 3 * time.Second - -// var broadcastAddr = "224.0.0.1:1679" // 多播地址 -var broadcastAddr = "224.0.0.251:1234" - -// var broadcastAddr = "255.255.255.255:1769" // 广播地址 -var OnlineUsers = make(map[string]UdpMessage) // 全局map,key为IP,value为主机名 diff --git a/godo/localchat/udp.go b/godo/localchat/udp.go new file mode 100644 index 0000000..75687aa --- /dev/null +++ b/godo/localchat/udp.go @@ -0,0 +1,168 @@ +package localchat + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + + "golang.org/x/net/ipv4" +) + +type UdpMessage struct { + Type string `json:"type"` + IP string `json:"ip"` + Hostname string `json:"hostname"` + Message any `json:"message"` +} + +// 多播地址列表 +var multicastAddrs = []string{"239.255.255.250:2024", "239.255.255.251:2024", "224.0.0.251:1234", "224.0.0.1:1679"} +var OnlineUsers = make(map[string]UdpMessage) // 全局map,key为IP,value为主机名 +// SendMulticast 发送多播消息 +func init() { + go InitMulticast() + go ListenForMulticast() +} +func InitMulticast() { + myIP, myHostname, err := GetMyIPAndHostname() + if err != nil { + return + } + message := UdpMessage{ + Type: "online", + IP: myIP, + Hostname: myHostname, + Message: "online", + } + SendMulticast(message) +} +func SendMulticast(message UdpMessage) error { + for _, addrStr := range multicastAddrs { + addr, err := net.ResolveUDPAddr("udp4", addrStr) + if err != nil { + return err + } + + conn, err := net.DialUDP("udp4", nil, addr) + if err != nil { + return err + } + defer conn.Close() + + data, err := json.Marshal(message) + if err != nil { + return err + } + + _, err = conn.WriteToUDP(data, addr) + if err != nil { + return err + } + + fmt.Printf("发送多播消息到 %s 成功\n", addrStr) + } + + return nil +} + +// ListenForMulticast 监听多播消息 +func ListenForMulticast() { + multicastGroup, err := net.ResolveUDPAddr("udp4", multicastAddrs[0]) + if err != nil { + fmt.Println("Error resolving UDP address:", err) + return + } + + conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 2024}) + if err != nil { + fmt.Println("Error listening on UDP address:", err) + return + } + defer conn.Close() + udpConn := ipv4.NewPacketConn(conn) + if err := udpConn.JoinGroup(nil, multicastGroup); err != nil { + log.Fatalf("Failed to join multicast group: %v", err) + } + + buffer := make([]byte, 1024) + for { + n, _, src, err := udpConn.ReadFrom(buffer) + if err != nil { + log.Printf("Error reading from UDP: %v, addr: %v", err, src) + continue + } + + var udpMsg UdpMessage + err = json.Unmarshal(buffer[:n], &udpMsg) + if err != nil { + fmt.Printf("Error unmarshalling JSON: %v\n", err) + continue + } + + OnlineUsers[udpMsg.IP] = udpMsg + if udpMsg.Type == "file" { + RecieveFile(udpMsg) + } + fmt.Printf("Received message from %s: %s\n", udpMsg.IP, udpMsg.Hostname) + } +} + +// SendToIP 向指定的 IP 地址发送 UDP 消息 +func SendToIP(ip string, message UdpMessage) error { + addr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:2024", ip)) + if err != nil { + return err + } + + conn, err := net.DialUDP("udp4", nil, addr) + if err != nil { + return err + } + defer conn.Close() + + data, err := json.Marshal(message) + if err != nil { + return err + } + + _, err = conn.WriteToUDP(data, addr) + if err != nil { + return err + } + + fmt.Printf("发送 UDP 消息到 %s 成功\n", ip) + return nil +} + +// 获取 OnlineUsers 的最新状态 +func GetOnlineUsers() map[string]UdpMessage { + return OnlineUsers +} +func HandleMessage(w http.ResponseWriter, r *http.Request) { + var msg UdpMessage + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&msg); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + ip := msg.IP + preferredIP, err := GetMyIp() + if err != nil { + http.Error(w, "Failed to get preferred IP", http.StatusInternalServerError) + return + } + msg.IP = preferredIP + //msg.Type = "text" + err = SendToIP(ip, msg) + if err != nil { + http.Error(w, "Failed to send message", http.StatusInternalServerError) + return + } + //log.Printf("Received text message from %s: %s", msg.SenderInfo.IP, msg.Content) + // 这里可以添加存储文本消息到数据库或其他处理逻辑 + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "Text message send successfully") +} diff --git a/godo/localchat/upload.go b/godo/localchat/upload.go deleted file mode 100644 index 9ac4e56..0000000 --- a/godo/localchat/upload.go +++ /dev/null @@ -1,205 +0,0 @@ -package localchat - -import ( - "encoding/base64" - "fmt" - "godo/libs" - "io" - "log" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" -) - -// 合并文件分片 -func mergeFiles(fileName string, totalParts int, chatDir string) error { - var parts []io.Reader - for i := 1; i <= totalParts; i++ { - filePath := fmt.Sprintf("%v%v_%v.part", chatDir, fileName, i) - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open part %d: %w", i, err) - } - defer file.Close() - parts = append(parts, file) - } - - mergedFilePath := fmt.Sprintf("%vmerged_%v", chatDir, fileName) - mergedFile, err := os.Create(mergedFilePath) - if err != nil { - return fmt.Errorf("failed to create merged file: %w", err) - } - defer mergedFile.Close() - - _, err = io.Copy(mergedFile, io.MultiReader(parts...)) - if err != nil { - return fmt.Errorf("failed to merge files: %w", err) - } - - // 合并后清理分片文件 - for i := 1; i <= totalParts; i++ { - os.Remove(fmt.Sprintf("%v%v_%v.part", chatDir, fileName, i)) - } - - return nil -} - -func UploadBigFileHandler(w http.ResponseWriter, r *http.Request, msg Message) { - - chatDir, err := GetChatPath() - if err != nil { - http.Error(w, "Failed to get chat path", http.StatusInternalServerError) - return - } - - // 创建或打开临时文件以写入分片 - tempFilePath := fmt.Sprintf("%v%v_%v.part", chatDir, msg.FileInfo.FileName, msg.FileInfo.PartNumber) - out, err := os.Create(tempFilePath) - if err != nil { - log.Printf("Failed to create temp file: %v", err) - http.Error(w, "Failed to create temp file", http.StatusInternalServerError) - return - } - defer out.Close() - - // 将请求体的内容写入临时文件 - _, err = io.Copy(out, r.Body) - if err != nil { - log.Printf("Failed to write file part: %v", err) - http.Error(w, "Failed to write file part", http.StatusInternalServerError) - return - } - - // 更新上传状态 - uploadStatus.Lock() - uploadStatus.Status[msg.FileInfo.FileName]++ - if uploadStatus.Status[msg.FileInfo.FileName] == msg.FileInfo.TotalParts { - // 所有分片上传完成,触发合并 - go func() { - err := mergeFiles(msg.FileInfo.FileName, msg.FileInfo.TotalParts, chatDir) - if err != nil { - log.Printf("Failed to merge files for %v: %v", msg.FileInfo.FileName, err) - } else { - log.Printf("Merged file %v successfully", msg.FileInfo.FileName) - } - // 清理状态记录 - delete(uploadStatus.Status, msg.FileInfo.FileName) - msg.Content = "uploaded" - messageChan <- msg - }() - } - uploadStatus.Unlock() - - // 返回成功响应 - w.WriteHeader(http.StatusCreated) - fmt.Fprintln(w, "File part uploaded successfully") -} - -// SaveContentToFile 保存内容到文件并返回UploadInfo结构体 -func SaveContentToFile(content, fileName string) (UploadInfo, error) { - uploadBaseDir, err := GetChatPath() - if err != nil { - return UploadInfo{}, err - } - appDir, err := libs.GetOsDir() - if err != nil { - return UploadInfo{}, err - } - - // 去除文件名中的空格 - fileNameWithoutSpaces := strings.ReplaceAll(fileName, " ", "_") - fileNameWithoutSpaces = strings.ReplaceAll(fileNameWithoutSpaces, "/", "") - fileNameWithoutSpaces = strings.ReplaceAll(fileNameWithoutSpaces, `\`, "") - // 提取文件名和扩展名 - // 查找最后一个点的位置 - lastDotIndex := strings.LastIndexByte(fileNameWithoutSpaces, '.') - - // 如果找到点,则提取扩展名,否则视为没有扩展名 - ext := "" - if lastDotIndex != -1 { - ext = fileNameWithoutSpaces[lastDotIndex:] - fileNameWithoutSpaces = fileNameWithoutSpaces[:lastDotIndex] - } else { - ext = "" - } - randFileName := fmt.Sprintf("%s_%s%s", fileNameWithoutSpaces, strconv.FormatInt(time.Now().UnixNano(), 10), ext) - savePath := filepath.Join(uploadBaseDir, randFileName) - - if err := os.MkdirAll(filepath.Dir(savePath), 0755); err != nil { - return UploadInfo{}, err - } - - if err := os.WriteFile(savePath, []byte(content), 0644); err != nil { - return UploadInfo{}, err - } - content = string(content) - // 检查文件内容是否以"link::"开头 - if !strings.HasPrefix(content, "link::") { - content = base64.StdEncoding.EncodeToString([]byte(content)) - } - return UploadInfo{ - Name: fileNameWithoutSpaces, - SavePath: strings.TrimPrefix(savePath, appDir), - Content: content, - CreatedAt: time.Now(), - }, nil -} - -// MultiUploadHandler 处理多文件上传请求 -func MultiUploadHandler(w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(10000 << 20); err != nil { - http.Error(w, "Failed to parse multipart form", http.StatusBadRequest) - return - } - - files := r.MultipartForm.File["files"] - if len(files) == 0 { - http.Error(w, "No file parts in the request", http.StatusBadRequest) - return - } - - fileInfoList := make([]UploadInfo, 0, len(files)) - - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - http.Error(w, "Failed to open uploaded file", http.StatusBadRequest) - continue - } - defer file.Close() - - content, err := io.ReadAll(file) - if err != nil { - http.Error(w, "Failed to read uploaded file", http.StatusBadRequest) - continue - } - //log.Printf(string(content)) - // 保存上传的文件内容 - info, err := SaveContentToFile(string(content), fileHeader.Filename) - if err != nil { - http.Error(w, "Failed to save uploaded file", http.StatusBadRequest) - continue - } - log.Println(info.SavePath) - - //info.SavePath = savePath - fileInfoList = append(fileInfoList, info) - } - user := UserInfo{ - IP: r.FormValue("ip"), - Hostname: r.FormValue("hostname"), - } - msg := Message{ - Type: "file", - Content: "file recieved", - SenderInfo: user, - FileList: fileInfoList, - } - messageChan <- msg - w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, "File send successfully") - //serv.Res(serv.Response{Code: 0, Data: fileInfoList}, w) -} diff --git a/godo/sys/msg.go b/godo/sys/msg.go index 28d8a66..9daabfe 100644 --- a/godo/sys/msg.go +++ b/godo/sys/msg.go @@ -3,6 +3,7 @@ package sys import ( "encoding/json" "fmt" + "godo/localchat" "net/http" "sync" "time" @@ -52,12 +53,20 @@ func HandleSystemEvents(w http.ResponseWriter, r *http.Request) { mutex.Unlock() }() - // 读取客户端请求直到关闭 - for { - select { - case <-r.Context().Done(): + // 使用定时器轮询客户端请求 + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for range ticker.C { + // 检查客户端是否已断开连接 + if r.Context().Err() != nil { return } + onlineUsers := localchat.GetOnlineUsers() + msg := Message{ + Type: "localchat", + Data: onlineUsers, + } + Broadcast(msg) } }