Browse Source

change localchat

master
godo 9 months ago
parent
commit
704245dd40
  1. 2
      frontend/components.d.ts
  2. 25
      frontend/src/components/builtin/FileList.vue
  3. 411
      frontend/src/components/desktop/LockDesktop.vue
  4. 90
      frontend/src/components/localchat/Chat.vue
  5. 18
      frontend/src/components/taskbar/StartOption.vue
  6. 2
      frontend/src/stores/choose.ts
  7. 15
      frontend/src/stores/localchat.ts
  8. 5
      frontend/src/stores/upgrade.ts
  9. 6
      godo/cmd/main.go
  10. 1
      godo/go.mod
  11. 1
      godo/go.sum
  12. 111
      godo/localchat/client.go
  13. 198
      godo/localchat/file.go
  14. 67
      godo/localchat/info.go
  15. 44
      godo/localchat/server.go
  16. 133
      godo/localchat/sse.go
  17. 63
      godo/localchat/type.go
  18. 168
      godo/localchat/udp.go
  19. 205
      godo/localchat/upload.go
  20. 17
      godo/sys/msg.go

2
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']

25
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()
},
})
}

411
frontend/src/components/desktop/LockDesktop.vue

@ -1,12 +1,104 @@
<template>
<div class="lockscreen" :class="lockClassName">
<el-card class="login-box" shadow="never">
<div class="avatar-container">
<el-avatar size="large">
<img src="/logo.png" alt="Logo" />
</el-avatar>
</div>
<el-form v-if="!isRegisterMode" label-position="left" label-width="0px">
<el-form-item>
<el-input
v-model="userName"
placeholder="请输入用户名"
autofocus
prefix-icon="UserFilled"
></el-input>
</el-form-item>
<el-form-item v-if="!sys._options.noPassword">
<el-input
v-model="userPassword"
type="password"
placeholder="请输入登录密码"
show-password
prefix-icon="Key"
@keyup.enter="onLogin"
></el-input>
</el-form-item>
<el-button type="primary" @click="onLogin">登录</el-button>
<div class="actions" v-if="config.userType === 'member'">
<a href="#" @click.prevent="toggleRegister">注册新用户</a>
<a href="#" @click.prevent="toggleUserSwitch">切换角色</a>
</div>
</el-form>
<el-form v-else label-position="left" label-width="0px" :model="regForm" ref="regFormRef" :rules="rules">
<el-form-item prop="username">
<el-input
v-model="regForm.username"
placeholder="请输入用户名"
prefix-icon="UserFilled"
></el-input>
</el-form-item>
<el-form-item prop="nickname">
<el-input
v-model="regForm.nickname"
placeholder="请输入真实姓名"
prefix-icon="Avatar"
></el-input>
</el-form-item>
<el-form-item prop="email">
<el-input
v-model="regForm.email"
placeholder="请输入邮箱"
prefix-icon="Message"
></el-input>
</el-form-item>
<el-form-item prop="phone">
<el-input
v-model="regForm.phone"
placeholder="请输入手机号"
prefix-icon="Iphone"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="regForm.password"
type="password"
placeholder="请输入密码"
show-password
prefix-icon="Key"
></el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="regForm.confirmPassword"
type="password"
placeholder="请再次输入密码"
show-password
prefix-icon="Lock"
></el-input>
</el-form-item>
<el-button type="primary" @click="onRegister">注册</el-button>
<div class="actions">
<a href="#" @click.prevent="toggleRegister">返回登录</a>
</div>
</el-form>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useSystem } from "@/system";
import { getSystemConfig, getClientId } from '@/system/config'
import { ref, onMounted } from 'vue';
import { useSystem } from '@/system';
import { getSystemConfig, setSystemConfig } from '@/system/config';
import { notifyError } from '@/util/msg';
import { RestartApp } from '@/util/goutil';
const sys = useSystem();
const loginCallback = sys._options.loginCallback;
const config = getSystemConfig();
const lockClassName = ref("screen-show");
const alertMsg = ref("");
const lockClassName = ref('screen-show');
const isRegisterMode = ref(false);
function loginSuccess() {
lockClassName.value = 'screen-hidean';
@ -14,103 +106,113 @@ function loginSuccess() {
lockClassName.value = 'screen-hide';
}, 500);
}
const userName = ref('');
const userPassword = ref('');
onMounted(() => {
if (config.userType == 'person') {
userName.value = sys._options.login?.username || "admin"
userPassword.value = sys._options.login?.password || ""
if (config.userType === 'person') {
userName.value = sys._options.login?.username || 'admin';
userPassword.value = sys._options.login?.password || '';
} else {
userName.value = config.userInfo.username
userPassword.value = config.userInfo.password
userName.value = config.userInfo.username;
userPassword.value = config.userInfo.password;
}
});
async function onLogin() {
if (loginCallback) {
alertMsg.value = "等待确认";
const res = await loginCallback(userName.value, userPassword.value);
if (res) {
loginSuccess();
const res = await loginCallback(userName.value, userPassword.value);
if (res) {
loginSuccess();
}
}
}
function toggleRegister() {
isRegisterMode.value = !isRegisterMode.value;
}
function toggleUserSwitch() {
config.userType = 'person'
setSystemConfig(config);
RestartApp();
}
const regForm = ref({
username: '',
password: '',
confirmPassword: '',
email: '',
phone: '',
nickname: '',
})
const regFormRef:any = ref(null);
const rules = {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度应在3到20个字符之间', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: (rule:any, value:any, callback:any) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== regForm.value.password) {
callback(new Error('两次输入的密码不一致'));
} else {
alertMsg.value = "密码错误";
callback();
}
}, trigger: 'blur' }
],
email: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
],
phone: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
],
nickname: [
{ required: true, message: '昵称不能为空', trigger: 'blur' },
{ min: 2, max: 20, message: '昵称长度应在2到20个字符之间', trigger: 'blur' }
]
};
async function onRegister() {
try {
await regFormRef.value.validate();
const save = toRaw(regForm.value);
const userInfo = config.userInfo;
const comp = await fetch(userInfo.url + '/member/register', {
method: 'POST',
body: JSON.stringify(save),
});
if(!comp.ok){
notifyError('网络错误,注册失败');
return
}
// if (config.userType == 'person') {
// if (loginCallback) {
// alertMsg.value = "";
// const res = await loginCallback(userName.value, userPassword.value);
// if (res) {
// loginSuccess();
// } else {
// alertMsg.value = "";
// }
// }
// } else {
// const serverUrl = config.userInfo.url + '/member/login'
// const res:any = await fetch(serverUrl, {
// method: "POST",
// body: JSON.stringify({
// username: userName.value,
// password: userPassword.value,
// clientId: getClientId(),
// }),
// });
// if(!res.ok){
// alertMsg.value = ""
// return
// }
// const jsondata = await res.json();
// if (jsondata.success) {
// loginSuccess();
// } else {
// alertMsg.value = jsondata.message
// }
// }
const res = await comp.json();
if(res.success){
notifyError('注册成功');
toggleRegister();
}else{
notifyError(res.message);
return
}
} catch (error) {
console.error(error);
}
}
</script>
<template>
<div class="lockscreen" :class="lockClassName">
<!---->
<div class="login-box">
<span class="ant-avatar ant-avatar-icon" style="width: 128px; height: 128px; line-height: 128px; font-size: 64px">
<span role="img" aria-label="user" class="anticon">
<svg focusable="false" width="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896">
<path
d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" />
</svg>
</span>
</span>
<div class="username">
<span class="ant-input-group">
<input placeholder="请输入用户名" autofocus class="ant-input" v-model="userName" />
</span>
</div>
<span>
<span class="ant-input-group">
<!---->
<input v-if="sys._options.noPassword !== true" placeholder="请输入登录密码" type="password" autofocus
class="ant-input" v-model="userPassword" />
<span class="ant-input-group-addon">
<button class="ant-btn-primary" type="button" @click="onLogin">
<!---->
<span class="anticon">
<svg focusable="false" width="1em" height="1em" fill="currentColor" aria-hidden="true"
viewBox="64 64 896 896">
<path
d="M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 000-48.4z" />
</svg>
</span>
</button>
</span>
</span>
<div class="tip">{{ alertMsg }}</div>
</span>
</div>
</div>
</template>
<style scoped lang="scss">
// @import '../../main.css';
.lockscreen {
position: absolute;
top: 0;
@ -119,101 +221,78 @@ async function onLogin() {
left: 0;
z-index: 201;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: #fff;
// background: #000;
background-color: rgba(25, 28, 34, 0.78);
-webkit-backdrop-filter: blur(7px);
backdrop-filter: blur(7px);
.login-box {
position: absolute;
top: 45%;
left: 50%;
display: flex;
transform: translate(-50%, -50%);
flex-direction: column;
justify-content: center;
align-items: center;
.ant-avatar {
text-align: center;
vertical-align: middle;
background: #ccc;
border-radius: 50%;
margin-bottom: 14px;
.anticon {
display: inline-block;
color: inherit;
font-style: normal;
line-height: 0;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
.login-box{
width: 300px;
padding: 20px;
text-align: center;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e0e0e0;
.ant-input-group {
display: flex;
.avatar-container {
margin-bottom: 20px;
}
.ant-input {
padding: 6px 11px;
font-size: 16px;
outline: none;
border: none;
}
.el-input {
width: 100%;
margin-bottom: 10px;
background: #f9f9f9;
border-radius: 4px;
}
.ant-btn-primary {
color: #fff;
background: #1890ff;
border-color: #1890ff;
border: none;
text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
box-shadow: 0 2px 0 rgb(0 0 0 / 5%);
height: 30px;
padding: 6.4px 15px;
font-size: 16px;
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
}
.el-button {
width: 100%;
margin-top: 10px;
background: #409eff;
color: #ffffff;
border: none;
border-radius: 4px;
transition: background 0.3s ease;
}
.ant-btn-primary:hover {
color: #fff;
background: #40a9ff;
border-color: #40a9ff;
}
.el-button:hover {
background: #66b1ff;
}
.ant-btn-primary:active {
color: #fff;
background: #096dd9;
border-color: #096dd9;
}
.tip {
padding: 4px 0;
font-size: 12px;
color: red;
height: 30px;
}
.username {
font-size: 30px;
margin-bottom: 14px;
.actions {
margin-top: 10px;
display: flex;
justify-content: space-between;
a {
color: #409eff;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}
.tip {
padding: 4px 0px;
font-size: 12px;
height: 30px;
.screen-hidean {
animation: outan 0.5s forwards;
}
}
.screen-hidean {
animation: outan 0.5s forwards;
}
.screen-hide {
display: none;
.screen-hide {
display: none;
}
}
@keyframes outan {
@ -230,4 +309,4 @@ async function onLogin() {
opacity: 0;
}
}
</style>
</style>

90
frontend/src/components/localchat/Chat.vue

@ -13,59 +13,59 @@
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import { onMounted } from "vue";
import { useLocalChatStore } from "@/stores/localchat";
import { ElMessage } from "element-plus";
import { getSystemConfig } from "@/system/config";
//import { ElMessage } from "element-plus";
//import { getSystemConfig } from "@/system/config";
const store = useLocalChatStore();
const config = getSystemConfig();
//const config = getSystemConfig();
let source:any;
//let source:any;
onMounted(async () => {
await store.init()
init()
//init()
});
onUnmounted(() => {
if (source) {
source.close();
}
});
function init() {
if (typeof EventSource === "undefined") {
ElMessage.error("您的浏览器不支持SSE");
return;
}
const sseUrl = config.apiUrl + "/localchat/sse";
source = new EventSource(sseUrl);
//
source.onmessage = async function (event:any) {
//console.log("has message!");
const eventData = event.data; //
const jsonData = JSON.parse(eventData); //
//console.log(jsonData);
if (jsonData.type == "user_list") {
//store.userList = jsonData;
store.setUserList(jsonData.content);
//await nextTick();
}
if(jsonData.type == 'text'){
store.addText(jsonData);
}
if(jsonData.type == 'file'){
store.addFile(jsonData);
}
};
//
source.onopen = function () {
console.log("Connection opened.");
};
// onUnmounted(() => {
// if (source) {
// source.close();
// }
// });
// function init() {
// if (typeof EventSource === "undefined") {
// ElMessage.error("SSE");
// return;
// }
// const sseUrl = config.apiUrl + "/localchat/sse";
// source = new EventSource(sseUrl);
// //
// source.onmessage = async function (event:any) {
// //console.log("has message!");
// const eventData = event.data; //
// const jsonData = JSON.parse(eventData); //
// //console.log(jsonData);
// if (jsonData.type == "user_list") {
// //store.userList = jsonData;
// store.setUserList(jsonData.content);
// //await nextTick();
// }
// if(jsonData.type == 'text'){
// store.addText(jsonData);
// }
// if(jsonData.type == 'file'){
// store.addFile(jsonData);
// }
// };
// //
// source.onopen = function () {
// console.log("Connection opened.");
// };
//
source.onerror = function () {
console.log("Connection closed.");
};
}
// //
// source.onerror = function () {
// console.log("Connection closed.");
// };
// }
</script>

18
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: {

2
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({

15
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: {

5
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);
}

6
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()

1
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 (

1
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=

111
godo/localchat/client.go

@ -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
}

198
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
}

67
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
}

44
godo/localchat/server.go

@ -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
}
}

133
godo/localchat/sse.go

@ -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)
}

63
godo/localchat/type.go

@ -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为主机名

168
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")
}

205
godo/localchat/upload.go

@ -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)
}

17
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)
}
}

Loading…
Cancel
Save