Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ae0ab09b48
|
|||
|
a3989aeedd
|
|||
|
ea40aa76d0
|
23
.golangci.yml
Normal file
23
.golangci.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
version: "2"
|
||||||
|
linters:
|
||||||
|
default: standard
|
||||||
|
enable:
|
||||||
|
- staticcheck
|
||||||
|
- gosec
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- gosec
|
||||||
|
text: "G304:"
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "is not checked"
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gci
|
||||||
|
- golines
|
||||||
|
output:
|
||||||
|
path-mode: abs
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
File,
|
||||||
FilesDroppedEvent
|
FilesDroppedEvent
|
||||||
} from "./models.js";
|
} from "./models.js";
|
||||||
|
|||||||
@@ -5,8 +5,33 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import { Create as $Create } from "@wailsio/runtime";
|
import { Create as $Create } from "@wailsio/runtime";
|
||||||
|
|
||||||
|
export class File {
|
||||||
|
"name": string;
|
||||||
|
"path": string;
|
||||||
|
|
||||||
|
/** Creates a new File instance. */
|
||||||
|
constructor($$source: Partial<File> = {}) {
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("path" in $$source)) {
|
||||||
|
this["path"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new File instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): File {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new File($$parsedSource as Partial<File>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FilesDroppedEvent {
|
export class FilesDroppedEvent {
|
||||||
"files": string[];
|
"files": File[];
|
||||||
"target": string;
|
"target": string;
|
||||||
|
|
||||||
/** Creates a new FilesDroppedEvent instance. */
|
/** Creates a new FilesDroppedEvent instance. */
|
||||||
@@ -25,7 +50,7 @@ export class FilesDroppedEvent {
|
|||||||
* Creates a new FilesDroppedEvent instance from a string or object.
|
* Creates a new FilesDroppedEvent instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): FilesDroppedEvent {
|
static createFrom($$source: any = {}): FilesDroppedEvent {
|
||||||
const $$createField0_0 = $$createType0;
|
const $$createField0_0 = $$createType1;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("files" in $$parsedSource) {
|
if ("files" in $$parsedSource) {
|
||||||
$$parsedSource["files"] = $$createField0_0($$parsedSource["files"]);
|
$$parsedSource["files"] = $$createField0_0($$parsedSource["files"]);
|
||||||
@@ -35,4 +60,5 @@ export class FilesDroppedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = $Create.Array($Create.Any);
|
const $$createType0 = File.createFrom;
|
||||||
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
|
|||||||
@@ -150,7 +150,18 @@ const handleCleanFinished = async () => {
|
|||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
<!-- 发现视图 -->
|
<!-- 发现视图 -->
|
||||||
<div v-show="activeKey === 'discover'">
|
<div v-show="activeKey === 'discover'">
|
||||||
<div v-if="peers.length > 0" class="peer-grid">
|
<div v-if="peers.length > 0">
|
||||||
|
<v-alert
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
density="compact"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
class="mb-4 text-body-2"
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
{{ t("discover.dragDropHint") }}
|
||||||
|
</v-alert>
|
||||||
|
<div class="peer-grid">
|
||||||
<div v-for="peer in peers" :key="peer.id">
|
<div v-for="peer in peers" :key="peer.id">
|
||||||
<PeerCard
|
<PeerCard
|
||||||
:peer="peer"
|
:peer="peer"
|
||||||
@@ -158,6 +169,7 @@ const handleCleanFinished = async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// --- Vue 核心 ---
|
// --- Vue 核心 ---
|
||||||
import { computed, ref, watch, onMounted } from "vue";
|
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
// --- 组件 ---
|
// --- 组件 ---
|
||||||
@@ -8,7 +8,7 @@ import FileSendModal from "./modals/FileSendModal.vue";
|
|||||||
import TextSendModal from "./modals/TextSendModal.vue";
|
import TextSendModal from "./modals/TextSendModal.vue";
|
||||||
|
|
||||||
// --- Wails & 后端绑定 ---
|
// --- Wails & 后端绑定 ---
|
||||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
import { Dialogs, Clipboard, Events } from "@wailsio/runtime";
|
||||||
import {
|
import {
|
||||||
SendFolder,
|
SendFolder,
|
||||||
SendText,
|
SendText,
|
||||||
@@ -19,14 +19,24 @@ import {
|
|||||||
AddTrust,
|
AddTrust,
|
||||||
RemoveTrust,
|
RemoveTrust,
|
||||||
} from "../../bindings/mesh-drop/internal/config/config";
|
} from "../../bindings/mesh-drop/internal/config/config";
|
||||||
|
import { File } from "bindings/mesh-drop/models";
|
||||||
|
|
||||||
// --- 生命周期 ---
|
// --- 生命周期 ---
|
||||||
|
const droppedFiles = ref<File[]>([]);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
isTrusted.value = await IsTrusted(props.peer.id);
|
isTrusted.value = await IsTrusted(props.peer.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to check trusted peer status:", err);
|
console.error("Failed to check trusted peer status:", err);
|
||||||
}
|
}
|
||||||
|
Events.On("files-dropped", (event) => {
|
||||||
|
droppedFiles.value = event.data.files;
|
||||||
|
showFileModal.value = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
Events.Off("files-dropped");
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 属性 & 事件 ---
|
// --- 属性 & 事件 ---
|
||||||
@@ -171,7 +181,14 @@ const handleUntrust = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-card hover link class="peer-card pa-2" :ripple="false">
|
<v-card
|
||||||
|
hover
|
||||||
|
link
|
||||||
|
class="peer-card pa-2"
|
||||||
|
:ripple="false"
|
||||||
|
data-file-drop-target
|
||||||
|
:id="`drop-zone-peer-${peer.id}`"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
|
<v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
|
||||||
@@ -219,6 +236,16 @@ const handleUntrust = () => {
|
|||||||
{{ t("discover.noRoute") }}
|
{{ t("discover.noRoute") }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 拖放提示覆盖层 -->
|
||||||
|
<div class="drag-drop-overlay">
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-file-upload-outline"
|
||||||
|
size="48"
|
||||||
|
color="primary"
|
||||||
|
style="opacity: 0.8"
|
||||||
|
></v-icon>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
@@ -304,6 +331,7 @@ const handleUntrust = () => {
|
|||||||
v-model="showFileModal"
|
v-model="showFileModal"
|
||||||
:peer="peer"
|
:peer="peer"
|
||||||
:selectedIp="selectedIp"
|
:selectedIp="selectedIp"
|
||||||
|
:files="droppedFiles"
|
||||||
@transferStarted="emit('transferStarted')"
|
@transferStarted="emit('transferStarted')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -314,3 +342,70 @@ const handleUntrust = () => {
|
|||||||
@transferStarted="emit('transferStarted')"
|
@transferStarted="emit('transferStarted')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.peer-card {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-card::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgb(var(--v-theme-primary));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-card.file-drop-target-active {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px -4px rgba(var(--v-theme-primary), 0.24) !important;
|
||||||
|
border-color: rgb(var(--v-theme-primary)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-card.file-drop-target-active::after {
|
||||||
|
opacity: 0.12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.8);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-card.file-drop-target-active .drag-drop-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-content {
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-card.file-drop-target-active .drag-drop-content {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// --- Vue 核心 ---
|
// --- Vue 核心 ---
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
// --- Wails & 后端绑定 ---
|
// --- Wails & 后端绑定 ---
|
||||||
import { Events, Dialogs } from "@wailsio/runtime";
|
import { Events, Dialogs, Window } from "@wailsio/runtime";
|
||||||
import { SendFiles } from "../../../bindings/mesh-drop/internal/transfer/service";
|
import { SendFiles } from "../../../bindings/mesh-drop/internal/transfer/service";
|
||||||
import { Peer } from "../../../bindings/mesh-drop/internal/discovery/models";
|
import { Peer } from "../../../bindings/mesh-drop/internal/discovery/models";
|
||||||
|
import { File } from "bindings/mesh-drop/models";
|
||||||
|
|
||||||
|
onMounted(() => {});
|
||||||
|
|
||||||
// --- 属性 & 事件 ---
|
// --- 属性 & 事件 ---
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
peer: Peer;
|
peer: Peer;
|
||||||
selectedIp: string;
|
selectedIp: string;
|
||||||
|
files: File[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -22,7 +26,6 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
// --- 状态 ---
|
// --- 状态 ---
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const fileList = ref<{ name: string; path: string }[]>([]);
|
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
const show = computed({
|
const show = computed({
|
||||||
@@ -34,19 +37,15 @@ const show = computed({
|
|||||||
watch(show, (newVal) => {
|
watch(show, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
Events.On("files-dropped", (event) => {
|
Events.On("files-dropped", (event) => {
|
||||||
const files: string[] = event.data.files || [];
|
const files: File[] = event.data.files || [];
|
||||||
files.forEach((f) => {
|
files.forEach((f) => {
|
||||||
if (!fileList.value.find((existing) => existing.path === f)) {
|
if (!props.files.find((existing) => existing.path === f.path)) {
|
||||||
fileList.value.push({
|
props.files.push(f);
|
||||||
name: f.split(/[\/]/).pop() || f,
|
|
||||||
path: f,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Events.Off("files-dropped");
|
Events.Off("files-dropped");
|
||||||
fileList.value = [];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,17 +59,17 @@ const openFileDialog = async () => {
|
|||||||
if (files) {
|
if (files) {
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
files.forEach((f) => {
|
files.forEach((f) => {
|
||||||
if (!fileList.value.find((existing) => existing.path === f)) {
|
if (!props.files.find((existing) => existing.path === f)) {
|
||||||
fileList.value.push({
|
props.files.push({
|
||||||
name: f.split(/[\\/]/).pop() || f,
|
name: f.split(/[\/]/).pop() || f,
|
||||||
path: f,
|
path: f,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const f = files as string;
|
const f = files as string;
|
||||||
if (!fileList.value.find((existing) => existing.path === f)) {
|
if (!props.files.find((existing) => existing.path === f)) {
|
||||||
fileList.value.push({
|
props.files.push({
|
||||||
name: f.split(/[\\/]/).pop() || f,
|
name: f.split(/[\\/]/).pop() || f,
|
||||||
path: f,
|
path: f,
|
||||||
});
|
});
|
||||||
@@ -80,12 +79,12 @@ const openFileDialog = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = (index: number) => {
|
const handleRemoveFile = (index: number) => {
|
||||||
fileList.value.splice(index, 1);
|
props.files.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendFiles = async () => {
|
const handleSendFiles = async () => {
|
||||||
if (fileList.value.length === 0 || !props.selectedIp) return;
|
if (props.files.length === 0 || !props.selectedIp) return;
|
||||||
const paths = fileList.value.map((f) => f.path);
|
const paths = props.files.map((f) => f.path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await SendFiles(props.peer, props.selectedIp, paths);
|
await SendFiles(props.peer, props.selectedIp, paths);
|
||||||
@@ -103,10 +102,11 @@ const handleSendFiles = async () => {
|
|||||||
<v-card :title="$t('modal.fileSend.title')">
|
<v-card :title="$t('modal.fileSend.title')">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div
|
<div
|
||||||
v-if="fileList.length === 0"
|
v-if="props.files.length === 0"
|
||||||
class="drop-zone pa-10 text-center rounded-lg border-dashed"
|
class="drop-zone pa-10 text-center rounded-lg"
|
||||||
@click="openFileDialog"
|
@click="openFileDialog"
|
||||||
data-file-drop-target
|
data-file-drop-target
|
||||||
|
id="drop-zone-area"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
icon="mdi-cloud-upload"
|
icon="mdi-cloud-upload"
|
||||||
@@ -127,9 +127,10 @@ const handleSendFiles = async () => {
|
|||||||
max-height="400"
|
max-height="400"
|
||||||
style="overflow-y: auto"
|
style="overflow-y: auto"
|
||||||
data-file-drop-target
|
data-file-drop-target
|
||||||
|
id="drop-zone-list"
|
||||||
>
|
>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="(file, index) in fileList"
|
v-for="(file, index) in props.files"
|
||||||
:key="file.path"
|
:key="file.path"
|
||||||
:title="file.name"
|
:title="file.name"
|
||||||
:subtitle="file.path"
|
:subtitle="file.path"
|
||||||
@@ -149,8 +150,7 @@ const handleSendFiles = async () => {
|
|||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
block
|
block
|
||||||
variant="outlined"
|
variant="tonal"
|
||||||
style="border-style: dashed"
|
|
||||||
prepend-icon="mdi-plus"
|
prepend-icon="mdi-plus"
|
||||||
@click="openFileDialog"
|
@click="openFileDialog"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
@@ -168,10 +168,10 @@ const handleSendFiles = async () => {
|
|||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="handleSendFiles"
|
@click="handleSendFiles"
|
||||||
:disabled="fileList.length === 0"
|
:disabled="props.files.length === 0"
|
||||||
>
|
>
|
||||||
{{ $t("modal.fileSend.sendSrc") }}
|
{{ $t("modal.fileSend.sendSrc") }}
|
||||||
{{ fileList.length > 0 ? `(${fileList.length})` : "" }}
|
{{ props.files.length > 0 ? `(${props.files.length})` : "" }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -180,17 +180,32 @@ const handleSendFiles = async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.drop-zone {
|
.drop-zone {
|
||||||
border: 2px dashed #666; /* Use a darker color or theme var */
|
border: 2px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone:hover {
|
.drop-zone:hover {
|
||||||
border-color: #38bdf8;
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
background-color: rgba(56, 189, 248, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone.file-drop-target-active {
|
.drop-zone.file-drop-target-active {
|
||||||
border-color: #38bdf8;
|
border-color: rgb(var(--v-theme-primary));
|
||||||
background-color: rgba(56, 189, 248, 0.1);
|
background-color: rgba(var(--v-theme-primary), 0.12);
|
||||||
|
transform: scale(1.01);
|
||||||
|
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#drop-zone-list {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drop-zone-list.file-drop-target-active {
|
||||||
|
box-shadow: inset 0 0 0 2px rgb(var(--v-theme-primary));
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"trustPeer": "Trust Peer",
|
"trustPeer": "Trust Peer",
|
||||||
"untrustPeer": "Untrust Peer",
|
"untrustPeer": "Untrust Peer",
|
||||||
"sendFolderFailed": "Failed to send folder: {error}",
|
"sendFolderFailed": "Failed to send folder: {error}",
|
||||||
"sendClipboardFailed": "Failed to send clipboard: {error}"
|
"sendClipboardFailed": "Failed to send clipboard: {error}",
|
||||||
|
"dragDropHint": "Drag and drop files here to send"
|
||||||
},
|
},
|
||||||
"transfers": {
|
"transfers": {
|
||||||
"noTransfers": "No transfers yet",
|
"noTransfers": "No transfers yet",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"trustPeer": "信任设备",
|
"trustPeer": "信任设备",
|
||||||
"untrustPeer": "取消信任",
|
"untrustPeer": "取消信任",
|
||||||
"sendFolderFailed": "发送文件夹失败: {error}",
|
"sendFolderFailed": "发送文件夹失败: {error}",
|
||||||
"sendClipboardFailed": "发送剪贴板失败: {error}"
|
"sendClipboardFailed": "发送剪贴板失败: {error}",
|
||||||
|
"dragDropHint": "拖放文件到此处快速发送"
|
||||||
},
|
},
|
||||||
"transfers": {
|
"transfers": {
|
||||||
"noTransfers": "暂无传输记录",
|
"noTransfers": "暂无传输记录",
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package config
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mesh-drop/internal/security"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"mesh-drop/internal/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WindowState 定义窗口状态
|
// WindowState 定义窗口状态
|
||||||
@@ -66,7 +66,7 @@ func GetUserHomeDir() string {
|
|||||||
// New 读取配置
|
// New 读取配置
|
||||||
func Load(defaultState WindowState) *Config {
|
func Load(defaultState WindowState) *Config {
|
||||||
configDir := GetConfigDir()
|
configDir := GetConfigDir()
|
||||||
_ = os.MkdirAll(configDir, 0755)
|
_ = os.MkdirAll(configDir, 0o750)
|
||||||
configFile := filepath.Join(configDir, "config.json")
|
configFile := filepath.Join(configDir, "config.json")
|
||||||
|
|
||||||
// 设置默认值
|
// 设置默认值
|
||||||
@@ -88,7 +88,9 @@ func Load(defaultState WindowState) *Config {
|
|||||||
TrustedPeer: make(map[string]string),
|
TrustedPeer: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBytes, err := os.ReadFile(configFile)
|
fileBytes, err := os.ReadFile(
|
||||||
|
configFile,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
slog.Error("Failed to read config file", "error", err)
|
slog.Error("Failed to read config file", "error", err)
|
||||||
@@ -107,7 +109,7 @@ func Load(defaultState WindowState) *Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保默认保存路径存在
|
// 确保默认保存路径存在
|
||||||
err = os.MkdirAll(defaultSavePath, 0755)
|
err = os.MkdirAll(defaultSavePath, 0o750)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create default save path", "path", defaultSavePath, "error", err)
|
slog.Error("Failed to create default save path", "path", defaultSavePath, "error", err)
|
||||||
}
|
}
|
||||||
@@ -145,7 +147,7 @@ func (c *Config) Save() error {
|
|||||||
|
|
||||||
func (c *Config) save() error {
|
func (c *Config) save() error {
|
||||||
dir := filepath.Dir(c.configPath)
|
dir := filepath.Dir(c.configPath)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +158,7 @@ func (c *Config) save() error {
|
|||||||
|
|
||||||
// 设置配置文件权限为 0600 (仅所有者读写)
|
// 设置配置文件权限为 0600 (仅所有者读写)
|
||||||
if c.configPath != "" {
|
if c.configPath != "" {
|
||||||
if err := os.WriteFile(c.configPath, jsonData, 0600); err != nil {
|
if err := os.WriteFile(c.configPath, jsonData, 0o600); err != nil {
|
||||||
slog.Warn("Failed to write config file", "error", err)
|
slog.Warn("Failed to write config file", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -181,7 +183,7 @@ func (c *Config) update(fn func()) {
|
|||||||
func (c *Config) SetSavePath(savePath string) {
|
func (c *Config) SetSavePath(savePath string) {
|
||||||
c.update(func() {
|
c.update(func() {
|
||||||
c.data.SavePath = savePath
|
c.data.SavePath = savePath
|
||||||
_ = os.MkdirAll(savePath, 0755)
|
_ = os.MkdirAll(savePath, 0o750)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,3 +61,20 @@ func (p *PresencePacket) SignPayload() []byte {
|
|||||||
// 格式: id|name|port|os|pk
|
// 格式: id|name|port|os|pk
|
||||||
return fmt.Appendf(nil, "%s|%s|%d|%s|%s", p.ID, p.Name, p.Port, p.OS, p.PublicKey)
|
return fmt.Appendf(nil, "%s|%s|%d|%s|%s", p.ID, p.Name, p.Port, p.OS, p.PublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopy 返回 Peer 的深拷贝
|
||||||
|
func (p Peer) DeepCopy() *Peer {
|
||||||
|
newPeer := p // 结构体浅拷贝 (值类型字段已复制)
|
||||||
|
|
||||||
|
// 手动深拷贝引用类型字段 (Routes)
|
||||||
|
if p.Routes != nil {
|
||||||
|
newPeer.Routes = make(map[string]*RouteState, len(p.Routes))
|
||||||
|
for k, v := range p.Routes {
|
||||||
|
// RouteState 只有值类型字段,但它是指针,所以需要新建对象并解引用赋值
|
||||||
|
stateCopy := *v
|
||||||
|
newPeer.Routes[k] = &stateCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newPeer
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mesh-drop/internal/config"
|
|
||||||
"mesh-drop/internal/security"
|
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -13,6 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"mesh-drop/internal/config"
|
||||||
|
"mesh-drop/internal/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -112,7 +112,13 @@ func (s *Service) GetLocalIPInSameSubnet(receiverIP string) (string, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slog.Error("Failed to get local IP in same subnet", "receiverIP", receiverIP, "component", "discovery")
|
slog.Error(
|
||||||
|
"Failed to get local IP in same subnet",
|
||||||
|
"receiverIP",
|
||||||
|
receiverIP,
|
||||||
|
"component",
|
||||||
|
"discovery",
|
||||||
|
)
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +228,13 @@ func (s *Service) startListening() {
|
|||||||
sigData := packet.SignPayload()
|
sigData := packet.SignPayload()
|
||||||
valid, err := security.Verify(packet.PublicKey, sigData, sig)
|
valid, err := security.Verify(packet.PublicKey, sigData, sig)
|
||||||
if err != nil || !valid {
|
if err != nil || !valid {
|
||||||
slog.Warn("Received invalid discovery packet signature", "id", packet.ID, "ip", remoteAddr.IP.String())
|
slog.Warn(
|
||||||
|
"Received invalid discovery packet signature",
|
||||||
|
"id",
|
||||||
|
packet.ID,
|
||||||
|
"ip",
|
||||||
|
remoteAddr.IP.String(),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +243,15 @@ func (s *Service) startListening() {
|
|||||||
trustedKeys := s.config.GetTrusted()
|
trustedKeys := s.config.GetTrusted()
|
||||||
if knownKey, ok := trustedKeys[packet.ID]; ok {
|
if knownKey, ok := trustedKeys[packet.ID]; ok {
|
||||||
if knownKey != packet.PublicKey {
|
if knownKey != packet.PublicKey {
|
||||||
slog.Warn("SECURITY ALERT: Peer ID mismatch with known public key (Spoofing attempt?)", "id", packet.ID, "known_key", knownKey, "received_key", packet.PublicKey)
|
slog.Warn(
|
||||||
|
"SECURITY ALERT: Peer ID mismatch with known public key (Spoofing attempt?)",
|
||||||
|
"id",
|
||||||
|
packet.ID,
|
||||||
|
"known_key",
|
||||||
|
knownKey,
|
||||||
|
"received_key",
|
||||||
|
packet.PublicKey,
|
||||||
|
)
|
||||||
trustMismatch = true
|
trustMismatch = true
|
||||||
// 当发现 ID 欺骗时,不更新 peer,而是标记为 trustMismatch
|
// 当发现 ID 欺骗时,不更新 peer,而是标记为 trustMismatch
|
||||||
// 用户可以手动重新添加信任
|
// 用户可以手动重新添加信任
|
||||||
@@ -337,7 +357,7 @@ func (s *Service) GetPeerByIP(ip string) (*Peer, bool) {
|
|||||||
|
|
||||||
for _, p := range s.peers {
|
for _, p := range s.peers {
|
||||||
if p.Routes[ip] != nil {
|
if p.Routes[ip] != nil {
|
||||||
return p, true
|
return p.DeepCopy(), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
@@ -348,7 +368,10 @@ func (s *Service) GetPeerByID(id string) (*Peer, bool) {
|
|||||||
defer s.peersMutex.RUnlock()
|
defer s.peersMutex.RUnlock()
|
||||||
|
|
||||||
peer, ok := s.peers[id]
|
peer, ok := s.peers[id]
|
||||||
return peer, ok
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return peer.DeepCopy(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetPeers() []Peer {
|
func (s *Service) GetPeers() []Peer {
|
||||||
@@ -357,7 +380,7 @@ func (s *Service) GetPeers() []Peer {
|
|||||||
|
|
||||||
list := make([]Peer, 0)
|
list := make([]Peer, 0)
|
||||||
for _, p := range s.peers {
|
for _, p := range s.peers {
|
||||||
list = append(list, *p)
|
list = append(list, *p.DeepCopy())
|
||||||
}
|
}
|
||||||
sort.Slice(list, func(i, j int) bool {
|
sort.Slice(list, func(i, j int) bool {
|
||||||
return list[i].Name < list[j].Name
|
return list[i].Name < list[j].Name
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ func generateSelfSignedCert(certPath, keyPath string) error {
|
|||||||
// 在实际的动态环境中,我们可能希望添加所有当前接口的 IP 地址
|
// 在实际的动态环境中,我们可能希望添加所有当前接口的 IP 地址
|
||||||
// 实际上,在客户端跳过 IP 验证对于本地 P2P 来说是很常见的。
|
// 实际上,在客户端跳过 IP 验证对于本地 P2P 来说是很常见的。
|
||||||
|
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
derBytes, err := x509.CreateCertificate(
|
||||||
|
rand.Reader,
|
||||||
|
&template,
|
||||||
|
&template,
|
||||||
|
&priv.PublicKey,
|
||||||
|
priv,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -73,7 +79,10 @@ func generateSelfSignedCert(certPath, keyPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer keyOut.Close()
|
defer keyOut.Close()
|
||||||
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
if err := pem.Encode(
|
||||||
|
keyOut,
|
||||||
|
&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)},
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
"mesh-drop/internal/discovery"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"mesh-drop/internal/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendFiles(target *discovery.Peer, targetIP string, filePaths []string) {
|
func (s *Service) SendFiles(target *discovery.Peer, targetIP string, filePaths []string) {
|
||||||
@@ -32,7 +32,15 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
|
|||||||
|
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to open file", "path", filePath, "error", err, "component", "transfer-client")
|
slog.Error(
|
||||||
|
"Failed to open file",
|
||||||
|
"path",
|
||||||
|
filePath,
|
||||||
|
"error",
|
||||||
|
err,
|
||||||
|
"component",
|
||||||
|
"transfer-client",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +109,15 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath
|
|||||||
|
|
||||||
size, err := calculateTarSize(ctx, folderPath)
|
size, err := calculateTarSize(ctx, folderPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to calculate folder size", "path", folderPath, "error", err, "component", "transfer-client")
|
slog.Error(
|
||||||
|
"Failed to calculate folder size",
|
||||||
|
"path",
|
||||||
|
folderPath,
|
||||||
|
"error",
|
||||||
|
err,
|
||||||
|
"component",
|
||||||
|
"transfer-client",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +153,13 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath
|
|||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
defer w.Close()
|
defer w.Close()
|
||||||
if err := streamFolderToTar(ctx, w, folderPath); err != nil {
|
if err := streamFolderToTar(ctx, w, folderPath); err != nil {
|
||||||
slog.Error("Failed to stream folder to tar", "error", err, "component", "transfer-client")
|
slog.Error(
|
||||||
|
"Failed to stream folder to tar",
|
||||||
|
"error",
|
||||||
|
err,
|
||||||
|
"component",
|
||||||
|
"transfer-client",
|
||||||
|
)
|
||||||
w.CloseWithError(err)
|
w.CloseWithError(err)
|
||||||
}
|
}
|
||||||
}(ctx)
|
}(ctx)
|
||||||
@@ -199,7 +221,12 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ask 向接收端发送传输请求
|
// ask 向接收端发送传输请求
|
||||||
func (s *Service) ask(ctx context.Context, target *discovery.Peer, targetIP string, task *Transfer) (TransferAskResponse, error) {
|
func (s *Service) ask(
|
||||||
|
ctx context.Context,
|
||||||
|
target *discovery.Peer,
|
||||||
|
targetIP string,
|
||||||
|
task *Transfer,
|
||||||
|
) (TransferAskResponse, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return TransferAskResponse{}, err
|
return TransferAskResponse{}, err
|
||||||
}
|
}
|
||||||
@@ -232,7 +259,14 @@ func (s *Service) ask(ctx context.Context, target *discovery.Peer, targetIP stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processTransfer 传输数据
|
// processTransfer 传输数据
|
||||||
func (s *Service) processTransfer(ctx context.Context, askResp TransferAskResponse, target *discovery.Peer, targetIP string, task *Transfer, payload io.Reader) {
|
func (s *Service) processTransfer(
|
||||||
|
ctx context.Context,
|
||||||
|
askResp TransferAskResponse,
|
||||||
|
target *discovery.Peer,
|
||||||
|
targetIP string,
|
||||||
|
task *Transfer,
|
||||||
|
payload io.Reader,
|
||||||
|
) {
|
||||||
defer func() {
|
defer func() {
|
||||||
s.NotifyTransferListUpdate()
|
s.NotifyTransferListUpdate()
|
||||||
}()
|
}()
|
||||||
@@ -240,7 +274,9 @@ func (s *Service) processTransfer(ctx context.Context, askResp TransferAskRespon
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uploadUrl, _ := url.Parse(fmt.Sprintf("https://%s:%d/transfer/upload/%s", targetIP, target.Port, task.ID))
|
uploadUrl, _ := url.Parse(
|
||||||
|
fmt.Sprintf("https://%s:%d/transfer/upload/%s", targetIP, target.Port, task.ID),
|
||||||
|
)
|
||||||
query := uploadUrl.Query()
|
query := uploadUrl.Query()
|
||||||
query.Add("token", askResp.Token)
|
query.Add("token", askResp.Token)
|
||||||
uploadUrl.RawQuery = query.Encode()
|
uploadUrl.RawQuery = query.Encode()
|
||||||
@@ -273,7 +309,15 @@ func (s *Service) processTransfer(ctx context.Context, askResp TransferAskRespon
|
|||||||
} else {
|
} else {
|
||||||
task.Status = TransferStatusError
|
task.Status = TransferStatusError
|
||||||
task.ErrorMsg = fmt.Sprintf("Failed to upload file: %v", err)
|
task.ErrorMsg = fmt.Sprintf("Failed to upload file: %v", err)
|
||||||
slog.Error("Failed to upload file", "url", uploadUrl.String(), "error", err, "component", "transfer-client")
|
slog.Error(
|
||||||
|
"Failed to upload file",
|
||||||
|
"url",
|
||||||
|
uploadUrl.String(),
|
||||||
|
"error",
|
||||||
|
err,
|
||||||
|
"component",
|
||||||
|
"transfer-client",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -384,7 +428,15 @@ func streamFolderToTar(ctx context.Context, w io.Writer, srcPath string) error {
|
|||||||
if relPath == "." {
|
if relPath == "." {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
slog.Debug("Processing file", "path", path, "relPath", relPath, "component", "transfer-client")
|
slog.Debug(
|
||||||
|
"Processing file",
|
||||||
|
"path",
|
||||||
|
path,
|
||||||
|
"relPath",
|
||||||
|
relPath,
|
||||||
|
"component",
|
||||||
|
"transfer-client",
|
||||||
|
)
|
||||||
|
|
||||||
header, err := tar.FileInfoHeader(info, "")
|
header, err := tar.FileInfoHeader(info, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package transfer
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mesh-drop/internal/config"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"mesh-drop/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SaveHistory() {
|
func (s *Service) SaveHistory() {
|
||||||
@@ -24,7 +25,7 @@ func (s *Service) SaveHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 写入临时文件
|
// 写入临时文件
|
||||||
if err := os.WriteFile(tempPath, historyJson, 0644); err != nil {
|
if err := os.WriteFile(tempPath, historyJson, 0o600); err != nil {
|
||||||
slog.Error("Failed to write temp history file", "error", err, "component", "transfer")
|
slog.Error("Failed to write temp history file", "error", err, "component", "transfer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package transfer
|
package transfer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mesh-drop/internal/discovery"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"mesh-drop/internal/discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferStatus string
|
type TransferStatus string
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ func (s *Service) handleAsk(c *gin.Context) {
|
|||||||
task.Sender.TrustMismatch = peer.TrustMismatch
|
task.Sender.TrustMismatch = peer.TrustMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.config.GetAutoAccept() || (s.config.IsTrusted(task.Sender.ID) && !task.Sender.TrustMismatch) {
|
if s.config.GetAutoAccept() ||
|
||||||
|
(s.config.IsTrusted(task.Sender.ID) && !task.Sender.TrustMismatch) {
|
||||||
task.DecisionChan <- Decision{
|
task.DecisionChan <- Decision{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
Accepted: true,
|
Accepted: true,
|
||||||
@@ -179,7 +180,15 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
_, err := os.Stat(destPath)
|
_, err := os.Stat(destPath)
|
||||||
counter := 1
|
counter := 1
|
||||||
for err == nil {
|
for err == nil {
|
||||||
destPath = filepath.Join(savePath, fmt.Sprintf("%s (%d)%s", strings.TrimSuffix(task.FileName, filepath.Ext(task.FileName)), counter, filepath.Ext(task.FileName)))
|
destPath = filepath.Join(
|
||||||
|
savePath,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s (%d)%s",
|
||||||
|
strings.TrimSuffix(task.FileName, filepath.Ext(task.FileName)),
|
||||||
|
counter,
|
||||||
|
filepath.Ext(task.FileName),
|
||||||
|
),
|
||||||
|
)
|
||||||
counter++
|
counter++
|
||||||
_, err = os.Stat(destPath)
|
_, err = os.Stat(destPath)
|
||||||
}
|
}
|
||||||
@@ -227,7 +236,13 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer Writer, ctxRead
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// 发送端断线,任务取消
|
// 发送端断线,任务取消
|
||||||
if c.Request.Context().Err() != nil {
|
if c.Request.Context().Err() != nil {
|
||||||
slog.Info("Sender canceled transfer (Network/Context disconnected)", "id", task.ID, "raw_err", err)
|
slog.Info(
|
||||||
|
"Sender canceled transfer (Network/Context disconnected)",
|
||||||
|
"id",
|
||||||
|
task.ID,
|
||||||
|
"raw_err",
|
||||||
|
err,
|
||||||
|
)
|
||||||
task.ErrorMsg = "Sender disconnected"
|
task.ErrorMsg = "Sender disconnected"
|
||||||
task.Status = TransferStatusCanceled
|
task.Status = TransferStatusCanceled
|
||||||
return
|
return
|
||||||
@@ -273,7 +288,12 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer Writer, ctxRead
|
|||||||
task.Status = TransferStatusCompleted
|
task.Status = TransferStatusCompleted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, ctxReader io.Reader) {
|
func (s *Service) receiveFolder(
|
||||||
|
c *gin.Context,
|
||||||
|
savePath string,
|
||||||
|
task *Transfer,
|
||||||
|
ctxReader io.Reader,
|
||||||
|
) {
|
||||||
defer s.NotifyTransferListUpdate()
|
defer s.NotifyTransferListUpdate()
|
||||||
|
|
||||||
// 创建根目录
|
// 创建根目录
|
||||||
@@ -286,7 +306,7 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer,
|
|||||||
counter++
|
counter++
|
||||||
_, err = os.Stat(destPath)
|
_, err = os.Stat(destPath)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(destPath, 0755); err != nil {
|
if err := os.MkdirAll(destPath, 0o750); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
Message: "Receiver failed to create folder",
|
Message: "Receiver failed to create folder",
|
||||||
@@ -318,7 +338,13 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if c.Request.Context().Err() != nil {
|
if c.Request.Context().Err() != nil {
|
||||||
slog.Info("Transfer canceled by sender (Network disconnect)", "id", task.ID, "stage", stage)
|
slog.Info(
|
||||||
|
"Transfer canceled by sender (Network disconnect)",
|
||||||
|
"id",
|
||||||
|
task.ID,
|
||||||
|
"stage",
|
||||||
|
stage,
|
||||||
|
)
|
||||||
task.Status = TransferStatusCanceled
|
task.Status = TransferStatusCanceled
|
||||||
task.ErrorMsg = "Sender disconnected"
|
task.ErrorMsg = "Sender disconnected"
|
||||||
// 发送端已断开,无需也不应再发送 c.JSON
|
// 发送端已断开,无需也不应再发送 c.JSON
|
||||||
@@ -350,6 +376,14 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取绝对路径以防止 Zip Slip (G305)
|
||||||
|
// 必须先转换成绝对路径再判断
|
||||||
|
absDestPath, err := filepath.Abs(destPath)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, "resolve_abs_path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tr := tar.NewReader(reader)
|
tr := tar.NewReader(reader)
|
||||||
for {
|
for {
|
||||||
header, err := tr.Next()
|
header, err := tr.Next()
|
||||||
@@ -360,32 +394,52 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
target := filepath.Join(destPath, header.Name)
|
target := filepath.Join(destPath, filepath.Clean(header.Name))
|
||||||
// 确保路径没有越界
|
absTarget, err := filepath.Abs(target)
|
||||||
if !strings.HasPrefix(target, filepath.Clean(destPath)+string(os.PathSeparator)) {
|
if err != nil {
|
||||||
// 非法路径
|
slog.Error("Failed to resolve absolute path", "path", target, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保路径在目标目录内
|
||||||
|
if !strings.HasPrefix(absTarget, absDestPath+string(os.PathSeparator)) {
|
||||||
|
slog.Warn(
|
||||||
|
"Zip Slip attempt detected",
|
||||||
|
"header_name",
|
||||||
|
header.Name,
|
||||||
|
"resolved_path",
|
||||||
|
absTarget,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用安全的绝对路径
|
||||||
|
target = absTarget
|
||||||
|
|
||||||
switch header.Typeflag {
|
switch header.Typeflag {
|
||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
if err := os.MkdirAll(target, 0755); err != nil {
|
if err := os.MkdirAll(target, 0o750); err != nil {
|
||||||
slog.Error("Failed to create dir", "path", target, "error", err)
|
slog.Error("Failed to create dir", "path", target, "error", err)
|
||||||
}
|
}
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
f, err := os.OpenFile(
|
||||||
|
target,
|
||||||
|
os.O_CREATE|os.O_RDWR,
|
||||||
|
os.FileMode(header.Mode),
|
||||||
|
) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create file", "path", target, "error", err)
|
slog.Error("Failed to create file", "path", target, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint: gosec
|
||||||
if _, err := io.Copy(f, tr); err != nil {
|
if _, err := io.Copy(f, tr); err != nil {
|
||||||
f.Close()
|
_ = f.Close()
|
||||||
if handleError(err, "write_file_content") {
|
if handleError(err, "write_file_content") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.Close()
|
_ = f.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mesh-drop/internal/config"
|
|
||||||
"mesh-drop/internal/discovery"
|
|
||||||
"mesh-drop/internal/security"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -16,6 +13,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
|
"mesh-drop/internal/config"
|
||||||
|
"mesh-drop/internal/discovery"
|
||||||
|
"mesh-drop/internal/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -37,12 +37,18 @@ type Service struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(config *config.Config, app *application.App, notifier *notifications.NotificationService, port int, discoveryService *discovery.Service) *Service {
|
func NewService(
|
||||||
|
config *config.Config,
|
||||||
|
app *application.App,
|
||||||
|
notifier *notifications.NotificationService,
|
||||||
|
port int,
|
||||||
|
discoveryService *discovery.Service,
|
||||||
|
) *Service {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
// 配置自定义 HTTP 客户端以跳过自签名证书验证
|
// 配置自定义 HTTP 客户端以跳过自签名证书验证
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
|
||||||
}
|
}
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Transport: tr,
|
Transport: tr,
|
||||||
@@ -95,7 +101,7 @@ func (s *Service) GetTransferSyncMap() *sync.Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetTransferList() []*Transfer {
|
func (s *Service) GetTransferList() []*Transfer {
|
||||||
var requests []*Transfer = make([]*Transfer, 0)
|
requests := make([]*Transfer, 0)
|
||||||
s.transfers.Range(func(key, value any) bool {
|
s.transfers.Range(func(key, value any) bool {
|
||||||
transfer := value.(*Transfer)
|
transfer := value.(*Transfer)
|
||||||
requests = append(requests, transfer)
|
requests = append(requests, transfer)
|
||||||
|
|||||||
41
main.go
41
main.go
@@ -3,14 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mesh-drop/internal/config"
|
|
||||||
"mesh-drop/internal/discovery"
|
|
||||||
"mesh-drop/internal/transfer"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/events"
|
"github.com/wailsapp/wails/v3/pkg/events"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
|
"mesh-drop/internal/config"
|
||||||
|
"mesh-drop/internal/discovery"
|
||||||
|
"mesh-drop/internal/transfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:frontend/dist
|
//go:embed all:frontend/dist
|
||||||
@@ -19,8 +20,13 @@ var assets embed.FS
|
|||||||
//go:embed build/appicon.png
|
//go:embed build/appicon.png
|
||||||
var icon []byte
|
var icon []byte
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
type FilesDroppedEvent struct {
|
type FilesDroppedEvent struct {
|
||||||
Files []string `json:"files"`
|
Files []File `json:"files"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +67,17 @@ func NewApp() *App {
|
|||||||
if screen != nil {
|
if screen != nil {
|
||||||
defaultWidth = int(float64(screen.Size.Width) * 0.8)
|
defaultWidth = int(float64(screen.Size.Width) * 0.8)
|
||||||
defaultHeight = int(float64(screen.Size.Height) * 0.8)
|
defaultHeight = int(float64(screen.Size.Height) * 0.8)
|
||||||
slog.Info("Primary screen found", "width", screen.Size.Width, "height", screen.Size.Height, "defaultWidth", defaultWidth, "defaultHeight", defaultHeight)
|
slog.Info(
|
||||||
|
"Primary screen found",
|
||||||
|
"width",
|
||||||
|
screen.Size.Width,
|
||||||
|
"height",
|
||||||
|
screen.Size.Height,
|
||||||
|
"defaultWidth",
|
||||||
|
defaultWidth,
|
||||||
|
"defaultHeight",
|
||||||
|
defaultHeight,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
slog.Info("No primary screen found, using defaults")
|
slog.Info("No primary screen found, using defaults")
|
||||||
}
|
}
|
||||||
@@ -131,14 +147,23 @@ func (a *App) registerCustomEvents() {
|
|||||||
|
|
||||||
func (a *App) setupEvents() {
|
func (a *App) setupEvents() {
|
||||||
// 窗口文件拖拽事件
|
// 窗口文件拖拽事件
|
||||||
a.mainWindows.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
|
a.mainWindows.OnWindowEvent(
|
||||||
files := event.Context().DroppedFiles()
|
events.Common.WindowFilesDropped,
|
||||||
|
func(event *application.WindowEvent) {
|
||||||
|
files := make([]File, 0)
|
||||||
|
for _, file := range event.Context().DroppedFiles() {
|
||||||
|
files = append(files, File{
|
||||||
|
Name: filepath.Base(file),
|
||||||
|
Path: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
details := event.Context().DropTargetDetails()
|
details := event.Context().DropTargetDetails()
|
||||||
a.app.Event.Emit("files-dropped", FilesDroppedEvent{
|
a.app.Event.Emit("files-dropped", FilesDroppedEvent{
|
||||||
Files: files,
|
Files: files,
|
||||||
Target: details.ElementID,
|
Target: details.ElementID,
|
||||||
})
|
})
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 窗口关闭事件
|
// 窗口关闭事件
|
||||||
a.mainWindows.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
a.mainWindows.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user