27 changed files with 2115 additions and 0 deletions
@ -0,0 +1,146 @@ |
|||
# If you prefer the allow list template instead of the deny list, see community template: |
|||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore |
|||
# |
|||
# Binaries for programs and plugins |
|||
*.exe |
|||
*.exe~ |
|||
*.dll |
|||
*.so |
|||
*.dylib |
|||
|
|||
# Test binary, built with `go test -c` |
|||
*.test |
|||
|
|||
# Output of the go coverage tool, specifically when used with LiteIDE |
|||
*.out |
|||
|
|||
# Dependency directories (remove the comment below to include it) |
|||
# vendor/ |
|||
|
|||
# Go workspace file |
|||
go.work |
|||
|
|||
vscode |
|||
.vscode |
|||
.vscode/* |
|||
!.vscode/settings.json |
|||
!.vscode/tasks.json |
|||
!.vscode/launch.json |
|||
!.vscode/extensions.json |
|||
!.vscode/*.code-snippets |
|||
|
|||
# Local History for Visual Studio Code |
|||
.history/ |
|||
|
|||
# Built Visual Studio Code Extensions |
|||
*.vsix |
|||
|
|||
# General |
|||
.DS_Store |
|||
.AppleDouble |
|||
.LSOverride |
|||
|
|||
# Icon must end with two \r |
|||
Icon |
|||
|
|||
|
|||
# Thumbnails |
|||
._* |
|||
|
|||
# Files that might appear in the root of a volume |
|||
.DocumentRevisions-V100 |
|||
.fseventsd |
|||
.Spotlight-V100 |
|||
.TemporaryItems |
|||
.Trashes |
|||
.VolumeIcon.icns |
|||
.com.apple.timemachine.donotpresent |
|||
|
|||
# Directories potentially created on remote AFP share |
|||
.AppleDB |
|||
.AppleDesktop |
|||
Network Trash Folder |
|||
Temporary Items |
|||
.apdisk |
|||
|
|||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider |
|||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 |
|||
|
|||
# User-specific stuff |
|||
.idea/**/workspace.xml |
|||
.idea/**/tasks.xml |
|||
.idea/**/usage.statistics.xml |
|||
.idea/**/dictionaries |
|||
.idea/**/shelf |
|||
|
|||
# AWS User-specific |
|||
.idea/**/aws.xml |
|||
|
|||
# Generated files |
|||
.idea/**/contentModel.xml |
|||
|
|||
# Sensitive or high-churn files |
|||
.idea/**/dataSources/ |
|||
.idea/**/dataSources.ids |
|||
.idea/**/dataSources.local.xml |
|||
.idea/**/sqlDataSources.xml |
|||
.idea/**/dynamic.xml |
|||
.idea/**/uiDesigner.xml |
|||
.idea/**/dbnavigator.xml |
|||
|
|||
# Gradle |
|||
.idea/**/gradle.xml |
|||
.idea/**/libraries |
|||
|
|||
# Gradle and Maven with auto-import |
|||
# When using Gradle or Maven with auto-import, you should exclude module files, |
|||
# since they will be recreated, and may cause churn. Uncomment if using |
|||
# auto-import. |
|||
# .idea/artifacts |
|||
# .idea/compiler.xml |
|||
# .idea/jarRepositories.xml |
|||
# .idea/modules.xml |
|||
# .idea/*.iml |
|||
# .idea/modules |
|||
# *.iml |
|||
# *.ipr |
|||
|
|||
# CMake |
|||
cmake-build-*/ |
|||
|
|||
# Mongo Explorer plugin |
|||
.idea/**/mongoSettings.xml |
|||
|
|||
# File-based project format |
|||
*.iws |
|||
|
|||
# IntelliJ |
|||
out/ |
|||
|
|||
# mpeltonen/sbt-idea plugin |
|||
.idea_modules/ |
|||
|
|||
# JIRA plugin |
|||
atlassian-ide-plugin.xml |
|||
|
|||
# Cursive Clojure plugin |
|||
.idea/replstate.xml |
|||
|
|||
# SonarLint plugin |
|||
.idea/sonarlint/ |
|||
|
|||
# Crashlytics plugin (for Android Studio and IntelliJ) |
|||
com_crashlytics_export_strings.xml |
|||
crashlytics.properties |
|||
crashlytics-build.properties |
|||
fabric.properties |
|||
|
|||
# Editor-based Rest Client |
|||
.idea/httpRequests |
|||
|
|||
# Android studio 3.1+ serialized cache file |
|||
.idea/caches/build_file_checksums.ser |
|||
|
|||
# Binary file |
|||
st |
|||
builds |
|||
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2023 chyok |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
@ -0,0 +1,26 @@ |
|||
FLAGS := -ldflags "-s -w" -trimpath |
|||
NOCGO := CGO_ENABLED=0 |
|||
|
|||
ci:: build-all |
|||
echo "done" |
|||
|
|||
build:: |
|||
go vet && go fmt |
|||
${NOCGO} go build ${FLAGS} -o xtfs |
|||
|
|||
build-all:: build |
|||
${NOCGO} GOOS=linux GOARCH=amd64 go build ${FLAGS} -o builds/xtfs-linux-x64 |
|||
${NOCGO} GOOS=linux GOARCH=arm go build ${FLAGS} -o builds/xtfs-linux-arm |
|||
${NOCGO} GOOS=linux GOARCH=arm64 go build ${FLAGS} -o builds/xtfs-linux-arm64 |
|||
${NOCGO} GOOS=darwin GOARCH=amd64 go build ${FLAGS} -o builds/xtfs-mac-x64 |
|||
${NOCGO} GOOS=darwin GOARCH=arm64 go build ${FLAGS} -o builds/xtfs-mac-arm64 |
|||
${NOCGO} GOOS=windows GOARCH=amd64 go build ${FLAGS} -o builds/xtfs-windows.exe |
|||
sha256sum builds/* |
|||
|
|||
clean:: |
|||
rm -f xtfs |
|||
rm -f xtfs-linux64 |
|||
rm -f xtfs-linux-arm |
|||
rm -f xtfs-linux-arm64 |
|||
rm -f xtfs-mac |
|||
rm -f xtfs-windows.exe |
|||
@ -0,0 +1,62 @@ |
|||
# Simple Transfer |
|||
|
|||
 |
|||
 |
|||
|
|||
`st` is a command-line file transfer tool for local networks. It has a built-in LAN discovery feature, allowing easy file sharing between devices. |
|||
|
|||
 |
|||
|
|||
## Simple Usage |
|||
1. **Receive Files**: - Run `st` to start the file reception service and display a QR code. - Another device can scan the QR code or access the displayed service address to upload files. |
|||
|
|||
2. **Send Files**: - Run `st [filename|foldername]` to start the file sending service and display a QR code. - Another device can scan the QR code or access the displayed service address to download the file. |
|||
|
|||
3. **Automatic discovery**: If both devices have `st` running: |
|||
|
|||
Device A: `st` |
|||
|
|||
Device B: `st xxx.txt` send file to A |
|||
|
|||
------ |
|||
|
|||
Device A: `st xxx.txt` |
|||
|
|||
Device B: `st` receive file from A |
|||
|
|||
## Features |
|||
|
|||
`st` offers a convenient and quick method for file transfer within a local network. |
|||
|
|||
- Web-based file transfer interface |
|||
- QR code for more convenient transfer between mobile phone and pc. |
|||
- Support for transferring both files and folders |
|||
- Automatic discovery of hosts within a local network |
|||
|
|||
## Installation |
|||
|
|||
### Binaries on macOS, Linux, Windows |
|||
|
|||
Download from [Github Releases](https://github.com/chyok/st/releases), add st to your $PATH. |
|||
|
|||
### Build from Source |
|||
|
|||
``` |
|||
go install github.com/chyok/st@latest |
|||
``` |
|||
|
|||
## Command |
|||
|
|||
`st` |
|||
start a receive server and display a QR code., waiting for upload. |
|||
|
|||
`st [filename|foldername]` |
|||
start a send server and display a QR code., waiting for download. |
|||
|
|||
`st -p [port]` |
|||
manually specify the service port and multicast port, the default is 53333. |
|||
|
|||
|
|||
## License |
|||
|
|||
MIT. See [LICENSE](https://github.com/chyok/st/blob/main/LICENSE). |
|||
@ -0,0 +1,73 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net" |
|||
"os" |
|||
) |
|||
|
|||
type Config struct { |
|||
DeviceName string |
|||
Port string |
|||
LocalIP string |
|||
MulticastAddress string |
|||
WildcardAddress string |
|||
FilePath string |
|||
Version string |
|||
} |
|||
|
|||
var G Config |
|||
|
|||
func (c *Config) SetConf(port string) error { |
|||
Hostname, err := os.Hostname() |
|||
if err != nil { |
|||
Hostname = "unknow device" |
|||
} |
|||
// 设备名称
|
|||
c.DeviceName = Hostname |
|||
c.Port = port |
|||
c.LocalIP, err = getLocalIP() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
// 多播地址
|
|||
c.MulticastAddress = fmt.Sprintf("224.0.0.169:%s", port) |
|||
// 广播地址
|
|||
c.WildcardAddress = fmt.Sprintf("0.0.0.0:%s", port) |
|||
c.FilePath = "./" |
|||
c.Version = "0.3.0" |
|||
return nil |
|||
} |
|||
|
|||
// 本地ip
|
|||
func getLocalIP() (string, error) { |
|||
addrs, err := net.InterfaceAddrs() |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
var ips []string |
|||
for _, address := range addrs { |
|||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { |
|||
if ipnet.IP.To4() != nil { |
|||
ips = append(ips, ipnet.IP.String()) |
|||
} |
|||
} |
|||
} |
|||
if len(ips) == 0 { |
|||
return "", fmt.Errorf("get local ip failed") |
|||
} else if len(ips) == 1 { |
|||
return ips[0], nil |
|||
} else { |
|||
// Select the one connected to the network
|
|||
// when there are multiple network interfaces
|
|||
|
|||
// Is there a better way?
|
|||
c, err := net.Dial("udp", "8.8.8.8:80") |
|||
if err != nil { |
|||
return ips[0], nil |
|||
} |
|||
defer c.Close() |
|||
return c.LocalAddr().(*net.UDPAddr).IP.String(), nil |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
module github.com/chyok/st |
|||
|
|||
go 1.21.1 |
|||
|
|||
require ( |
|||
github.com/schollz/progressbar/v3 v3.14.2 |
|||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e |
|||
github.com/urfave/cli/v2 v2.27.1 |
|||
) |
|||
|
|||
require ( |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect |
|||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect |
|||
github.com/rivo/uniseg v0.4.7 // indirect |
|||
github.com/russross/blackfriday/v2 v2.1.0 // indirect |
|||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect |
|||
golang.org/x/sys v0.19.0 // indirect |
|||
golang.org/x/term v0.19.0 // indirect |
|||
) |
|||
@ -0,0 +1,33 @@ |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= |
|||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= |
|||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= |
|||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= |
|||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= |
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= |
|||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= |
|||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= |
|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= |
|||
github.com/schollz/progressbar/v3 v3.14.2 h1:EducH6uNLIWsr560zSV1KrTeUb/wZGAHqyMFIEa99ks= |
|||
github.com/schollz/progressbar/v3 v3.14.2/go.mod h1:aQAZQnhF4JGFtRJiw/eobaXpsqpVQAftEQ+hLGXaRc4= |
|||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= |
|||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= |
|||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= |
|||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= |
|||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= |
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
|||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= |
|||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
|||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= |
|||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= |
|||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= |
|||
@ -0,0 +1,104 @@ |
|||
package discovery |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net" |
|||
"strings" |
|||
|
|||
"github.com/chyok/st/config" |
|||
"github.com/chyok/st/internal/transfer" |
|||
) |
|||
|
|||
const separator = "|" |
|||
|
|||
type Role string |
|||
|
|||
const ( |
|||
Sender Role = "sender" |
|||
Receiver Role = "receiver" |
|||
) |
|||
|
|||
func Listen(role Role, filePath string) { |
|||
addr, err := net.ResolveUDPAddr("udp", config.G.MulticastAddress) |
|||
|
|||
if err != nil { |
|||
fmt.Printf("Failed to resolve %s: %v\n", config.G.MulticastAddress, err) |
|||
return |
|||
} |
|||
conn, err := net.ListenMulticastUDP("udp", nil, addr) |
|||
if err != nil { |
|||
fmt.Printf("Failed to listen on %s: %v\n", config.G.MulticastAddress, err) |
|||
return |
|||
} |
|||
defer conn.Close() |
|||
|
|||
buf := make([]byte, 1024) |
|||
for { |
|||
n, src, err := conn.ReadFromUDP(buf) |
|||
|
|||
remoteAddr := src.IP.String() + ":" + config.G.Port |
|||
|
|||
if err != nil { |
|||
fmt.Printf("Failed to read from %s: %v\n", remoteAddr, err) |
|||
continue |
|||
} |
|||
|
|||
message := string(buf[:n]) |
|||
parts := strings.Split(message, separator) |
|||
if len(parts) != 2 { |
|||
fmt.Printf("Received malformed message from %s: %s\n", remoteAddr, message) |
|||
continue |
|||
} |
|||
|
|||
deviceName := parts[0] |
|||
remoteRole := Role(parts[1]) |
|||
switch remoteRole { |
|||
case Sender: |
|||
if role == Sender { |
|||
// 发现发送端
|
|||
fmt.Printf("Discovered Sender: %s (%s)\n", deviceName, remoteAddr) |
|||
go func() { |
|||
err := transfer.ReceiveFiles(remoteAddr) |
|||
if err != nil { |
|||
fmt.Printf("Receive file from %s error: %s\n", remoteAddr, err) |
|||
} |
|||
}() |
|||
} |
|||
case Receiver: |
|||
if role == Receiver { |
|||
// 发现接收端
|
|||
fmt.Printf("Discovered Receiver: %s (%s)\n", deviceName, remoteAddr) |
|||
go func() { |
|||
err := transfer.SendFiles(filePath, fmt.Sprintf("http://%s", remoteAddr)) |
|||
if err != nil { |
|||
fmt.Printf("Send file to %s error: %s\n", remoteAddr, err) |
|||
} |
|||
}() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
func Send(role Role) { |
|||
addr, err := net.ResolveUDPAddr("udp", config.G.MulticastAddress) |
|||
if err != nil { |
|||
fmt.Printf("Failed to resolve %s: %v\n", config.G.MulticastAddress, err) |
|||
return |
|||
} |
|||
|
|||
localAddr, err := net.ResolveUDPAddr("udp", config.G.LocalIP+":0") |
|||
if err != nil { |
|||
fmt.Printf("Failed to resolve %s: %v\n", config.G.MulticastAddress, err) |
|||
return |
|||
} |
|||
|
|||
conn, err := net.DialUDP("udp", localAddr, addr) |
|||
if err != nil { |
|||
fmt.Printf("Failed to dial %s: %v\n", config.G.MulticastAddress, err) |
|||
return |
|||
} |
|||
defer conn.Close() |
|||
|
|||
message := fmt.Sprintf("%s%s%s", config.G.DeviceName, separator, role) |
|||
conn.Write([]byte(message)) |
|||
} |
|||
@ -0,0 +1,351 @@ |
|||
package handler |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
"html/template" |
|||
"io" |
|||
"mime" |
|||
"net" |
|||
"path" |
|||
"strings" |
|||
"time" |
|||
|
|||
"net/http" |
|||
"net/rpc" |
|||
"os" |
|||
"path/filepath" |
|||
|
|||
"github.com/chyok/st/config" |
|||
"github.com/chyok/st/internal/transfer" |
|||
"github.com/chyok/st/internal/util" |
|||
"github.com/chyok/st/web" |
|||
) |
|||
|
|||
// rpc功能 压缩包的结构体
|
|||
// 文件路径 文件名
|
|||
type Args struct { |
|||
Zpfile, Fname string |
|||
} |
|||
|
|||
// 接收 上传
|
|||
func ReceiveHandler(w http.ResponseWriter, r *http.Request) { |
|||
switch r.Method { |
|||
case http.MethodGet: |
|||
// serve upload page for receive
|
|||
tmpl, err := template.New("index").Parse(web.UploadPage) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// 获取当前设备名称
|
|||
err = tmpl.Execute(w, config.G.DeviceName) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
} |
|||
case http.MethodPost: |
|||
// receive file and save
|
|||
file, header, err := r.FormFile("file") |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusBadRequest) |
|||
return |
|||
} |
|||
defer file.Close() |
|||
|
|||
_, params, err := mime.ParseMediaType(header.Header.Get("Content-Disposition")) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusBadRequest) |
|||
return |
|||
} |
|||
filename := filepath.FromSlash(params["filename"]) |
|||
|
|||
fmt.Printf("Downloading [%s]...\n", filename) |
|||
dirPath := filepath.Dir(filename) |
|||
err = os.MkdirAll(dirPath, os.ModePerm) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
out, err := os.Create(filename) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
defer out.Close() |
|||
|
|||
_, err = io.Copy(out, file) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// 如果收到的是zip文件,自动给解压缩
|
|||
suf := strings.Split(filename, ".") |
|||
if suf[1] == "zip" { |
|||
go util.DecompressZip(filename) |
|||
} |
|||
|
|||
fmt.Printf("[√] Download [%s] Success.\n", filename) |
|||
//
|
|||
default: |
|||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|||
} |
|||
} |
|||
|
|||
// 文件服务
|
|||
func FileServerHandler(w http.ResponseWriter, r *http.Request) { |
|||
currentPath := config.G.FilePath |
|||
|
|||
fileInfo, err := os.Stat(currentPath) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
basePath := filepath.Base(currentPath) |
|||
if fileInfo.IsDir() { |
|||
path := r.URL.Path[len("/download/"+basePath):] |
|||
fullPath := filepath.Join(currentPath, path) |
|||
http.ServeFile(w, r, fullPath) |
|||
} else { |
|||
http.ServeFile(w, r, currentPath) |
|||
} |
|||
} |
|||
|
|||
// udp 方式发送zip文件
|
|||
func SendZip(w http.ResponseWriter, r *http.Request) { |
|||
r.ParseForm() |
|||
// 选择文件,并生成zip包
|
|||
// 文件
|
|||
zipfarr := r.Form["zipfiles"] |
|||
// 服务器ip地址
|
|||
serip := r.Form["serverip"] |
|||
if serip[0] == "" { |
|||
http.Error(w, "remote server ip is blank!", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// 选中的路径
|
|||
wtculpath := r.Form["curpath"] |
|||
|
|||
// 实际路径
|
|||
realFilePath := filepath.Join(config.G.FilePath, wtculpath[0]) |
|||
|
|||
// zip 文件名
|
|||
zpFileName := "BIU_" + time.Now().Format("20060102_150405") + ".zip" |
|||
|
|||
// 创建zip 异步?
|
|||
taskId := make(chan string) |
|||
go func() { |
|||
util.CompressToZip(zpFileName, realFilePath, zipfarr) |
|||
taskId <- "arcok" |
|||
// fmt.Fprintln(w, "create archive:", err)
|
|||
}() |
|||
// go util.CompressToZip(zpFileName, realFilePath, zipfarr)
|
|||
fmt.Println("archive is createding...") |
|||
|
|||
// ZIP 文件的实际路径
|
|||
ziprl := path.Join(config.G.FilePath, "/files/", zpFileName) |
|||
// 检查信息
|
|||
// zpinfo, err := os.Stat(ziprl)
|
|||
// err ==nil {
|
|||
// fmt.Println("zip file :%s",err)
|
|||
// }
|
|||
// 停止下,检查zip是否创建成功
|
|||
// time.Sleep(1200)
|
|||
|
|||
// zip 创建成功后
|
|||
rest := <-taskId |
|||
// 有压缩包 才可以操作
|
|||
if strings.EqualFold(strings.ToLower(rest), "arcok") { |
|||
// 创建udp 渠道发送数据
|
|||
// 1、获取udp addr
|
|||
remoteAddr, err := net.ResolveUDPAddr("udp", serip[0]+":9099") |
|||
if err != nil { |
|||
fmt.Printf("Failed to resolve %s: %v\n", serip[0], err) |
|||
return |
|||
} |
|||
// 2、 监听端口
|
|||
conn, err := net.DialUDP("udp", nil, remoteAddr) |
|||
if err != nil { |
|||
fmt.Printf("Failed to dial %s: %v\n", serip[0], err) |
|||
return |
|||
} |
|||
defer conn.Close() |
|||
// 3、在端口发送数据
|
|||
message := fmt.Sprintf("%s%s%s", config.G.DeviceName, "|", "sender") |
|||
// 向链接通道发送数据 数据包头
|
|||
conn.Write([]byte(message)) |
|||
// 发送文件
|
|||
go func() { |
|||
err := transfer.SendFiles(ziprl, fmt.Sprintf("http://%s", remoteAddr)) |
|||
if err != nil { |
|||
fmt.Printf("Send file to %s error: %s\n", remoteAddr, err) |
|||
} |
|||
}() |
|||
|
|||
} else { |
|||
fmt.Println("archive is not exist!!!") |
|||
} |
|||
|
|||
} |
|||
|
|||
// 下载zip
|
|||
func Downzip(w http.ResponseWriter, r *http.Request) { |
|||
r.ParseForm() |
|||
// 选择文件,并生成zip包
|
|||
// 文件
|
|||
zipfarr := r.Form["zipfiles"] |
|||
// 服务器ip地址
|
|||
serip := r.Form["serverip"] |
|||
if serip[0] == "" { |
|||
http.Error(w, "remote server ip is blank!", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// fmt.Println(r.Form)
|
|||
// 选中的路径
|
|||
wtculpath := r.Form["curpath"] |
|||
|
|||
// 实际路径
|
|||
realFilePath := filepath.Join(config.G.FilePath, wtculpath[0]) |
|||
|
|||
// zip 文件名
|
|||
zpFileName := "BIU_" + time.Now().Format("20060102_150405") + ".zip" |
|||
|
|||
// 创建zip
|
|||
err := util.CompressToZip(zpFileName, realFilePath, zipfarr) |
|||
if err != nil { |
|||
fmt.Println("create zip error", err) |
|||
} |
|||
// 停止下,检查zip是否创建成功
|
|||
time.Sleep(1200) |
|||
_, err = os.Stat(realFilePath + "/" + zpFileName) |
|||
if err != nil { |
|||
|
|||
// 调用httpd rpc
|
|||
fmt.Println("RPC Server IP:", serip[0]) |
|||
rpcServer := serip[0] + ":9080" |
|||
client, err := rpc.DialHTTP("tcp", rpcServer) |
|||
if err != nil { |
|||
fmt.Println("Http rpc error :", err) |
|||
} |
|||
// 组装url 静态文件
|
|||
// url := "http://" + config.G.LocalIP + ":" + config.G.Port + "/files/" + zpFileName
|
|||
// 动态
|
|||
url := "http://" + config.G.LocalIP + ":" + config.G.Port + "/files?zp=" + zpFileName |
|||
args := Args{url, zpFileName} |
|||
var reply int |
|||
err = client.Call("Rbup.DecompressZip", args, &reply) |
|||
if err != nil { |
|||
fmt.Println("rpc call error :", err) |
|||
// 输出执行结果
|
|||
fmt.Fprintln(w, "rpc call error:", err) |
|||
} else { |
|||
// 输出下载链接
|
|||
fmt.Fprintf(w, "<a href='%s'>%s</a><br/>\n", url, url) |
|||
fmt.Fprintf(w, "RPC execute result :%d<br/>\n", reply) |
|||
} |
|||
} else { |
|||
fmt.Fprintf(w, "archive is created faild") |
|||
} |
|||
|
|||
} |
|||
|
|||
// 文件下载
|
|||
func Dfiles(w http.ResponseWriter, r *http.Request) { |
|||
// url中获取参数
|
|||
query := r.URL.Query() |
|||
zpfile := query.Get("zp") |
|||
|
|||
// 实际路径
|
|||
zpFileName := filepath.Join(config.G.FilePath, "/files/", zpfile) |
|||
// fileInfo, err := os.Stat(zpFileName)
|
|||
// if err != nil {
|
|||
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
|||
// return
|
|||
// }
|
|||
// fmt.Println(fileInfo)
|
|||
http.ServeFile(w, r, zpFileName) |
|||
} |
|||
|
|||
// 发送拦截
|
|||
func SendHandler(w http.ResponseWriter, r *http.Request) { |
|||
switch r.Method { |
|||
case http.MethodGet: |
|||
// serve download page for send
|
|||
realFilePath := filepath.Join(config.G.FilePath, r.URL.Path[1:]) |
|||
downloadPath := filepath.Join(filepath.Base(config.G.FilePath), r.URL.Path[1:]) |
|||
fileInfo, err := os.Stat(realFilePath) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
data := struct { |
|||
DeviceName string |
|||
IsDir bool |
|||
FileName string |
|||
DownloadPath string |
|||
UrlPath string |
|||
Files []os.DirEntry |
|||
}{ |
|||
DeviceName: config.G.DeviceName, |
|||
DownloadPath: downloadPath, |
|||
UrlPath: strings.TrimSuffix(r.URL.Path, "/"), |
|||
} |
|||
|
|||
if fileInfo.IsDir() { |
|||
data.IsDir = true |
|||
// 遍历目录
|
|||
files, err := os.ReadDir(realFilePath) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
data.Files = files |
|||
} else { |
|||
data.FileName = filepath.Base(realFilePath) |
|||
} |
|||
|
|||
// 文件列表模板
|
|||
tmpl, err := template.New("download").Parse(web.DownloadPage) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
err = tmpl.Execute(w, data) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
} |
|||
case http.MethodPost: |
|||
// return file or folder information in JSON format for convenient send to the recipient
|
|||
currentPath := config.G.FilePath |
|||
fileInfo, err := os.Stat(currentPath) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
|
|||
var pathInfo struct { |
|||
Type string `json:"type"` |
|||
Paths []string `json:"paths"` |
|||
} |
|||
|
|||
if fileInfo.IsDir() { |
|||
files, err := util.GetDirFilePaths(currentPath, true) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
pathInfo.Paths = files |
|||
pathInfo.Type = "dir" |
|||
} else { |
|||
pathInfo.Paths = []string{filepath.Base(currentPath)} |
|||
pathInfo.Type = "file" |
|||
} |
|||
|
|||
w.Header().Set("Content-Type", "application/json") |
|||
json.NewEncoder(w).Encode(pathInfo) |
|||
default: |
|||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
package transfer |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
|
|||
"io" |
|||
"net/http" |
|||
"os" |
|||
"path/filepath" |
|||
) |
|||
|
|||
// 接收文件
|
|||
func ReceiveFiles(remoteAddr string) error { |
|||
|
|||
resp, err := http.Post(fmt.Sprintf("http://%s/", remoteAddr), "", nil) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
if resp.StatusCode != http.StatusOK { |
|||
return fmt.Errorf("failed to get file info: %s", resp.Status) |
|||
} |
|||
|
|||
var pathInfo struct { |
|||
Type string `json:"type"` |
|||
Paths []string `json:"paths"` |
|||
} |
|||
|
|||
if err := json.NewDecoder(resp.Body).Decode(&pathInfo); err != nil { |
|||
return err |
|||
} |
|||
|
|||
if pathInfo.Type == "dir" { |
|||
fmt.Printf("Found directory with %d paths:\n", len(pathInfo.Paths)) |
|||
for _, path := range pathInfo.Paths { |
|||
fmt.Printf("- %s\n", path) |
|||
} |
|||
fmt.Print("Do you want to download the entire directory? [y/n] ") |
|||
var confirm string |
|||
if _, err := fmt.Scanln(&confirm); err != nil || (confirm != "y" && confirm != "Y") { |
|||
return nil |
|||
} |
|||
for _, path := range pathInfo.Paths { |
|||
if err := downloadFile(remoteAddr, path); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
} else { |
|||
if len(pathInfo.Paths) != 1 { |
|||
return fmt.Errorf("unexpected number of paths: %d", len(pathInfo.Paths)) |
|||
} |
|||
if err := downloadFile(remoteAddr, pathInfo.Paths[0]); err != nil { |
|||
return err |
|||
} |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// 下载文件
|
|||
func downloadFile(remoteAddr, path string) error { |
|||
path = filepath.ToSlash(path) |
|||
resp, err := http.Get(fmt.Sprintf("http://%s/download/%s", remoteAddr, path)) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
if resp.StatusCode != http.StatusOK { |
|||
return fmt.Errorf("failed to download file: %s", resp.Status) |
|||
} |
|||
|
|||
fmt.Printf("Downloading [%s]...\n", path) |
|||
out, err := os.Create(path) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer out.Close() |
|||
|
|||
_, err = io.Copy(out, resp.Body) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
fmt.Printf("[✅] Download [%s] Success.\n", path) |
|||
return nil |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
package transfer |
|||
|
|||
import ( |
|||
"bytes" |
|||
"fmt" |
|||
"io" |
|||
"mime/multipart" |
|||
"net/http" |
|||
"os" |
|||
"path" |
|||
"path/filepath" |
|||
"strings" |
|||
|
|||
"github.com/chyok/st/internal/util" |
|||
"github.com/schollz/progressbar/v3" |
|||
) |
|||
|
|||
// 发送文件
|
|||
func SendFiles(filePath string, url string) error { |
|||
filePath = filepath.ToSlash(filePath) |
|||
fileInfo, err := os.Stat(filePath) |
|||
if err != nil { |
|||
if os.IsNotExist(err) { |
|||
return fmt.Errorf("file [%s] not exist", filePath) |
|||
} |
|||
return fmt.Errorf("file [%s] error: %w", filePath, err) |
|||
} |
|||
|
|||
if fileInfo.IsDir() { |
|||
return postDirectory(filePath, url) |
|||
} |
|||
|
|||
return postFile(filePath, path.Base(filePath), url) |
|||
} |
|||
|
|||
// 传送文件夹
|
|||
func postDirectory(dirPath string, url string) error { |
|||
files, err := util.GetDirFilePaths(dirPath, false) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
fmt.Println("\nAll files in folder:") |
|||
for _, file := range files { |
|||
fmt.Println(file) |
|||
} |
|||
|
|||
var confirm string |
|||
fmt.Print("\nTransfer all files? [Y/N] ") |
|||
fmt.Scanln(&confirm) |
|||
if strings.ToLower(confirm) != "y" { |
|||
fmt.Print("\nCancel send all files ") |
|||
return nil |
|||
} |
|||
|
|||
for _, file := range files { |
|||
fileName, _ := filepath.Rel(dirPath, file) |
|||
fileName = filepath.Join(filepath.Base(dirPath), fileName) |
|||
err := postFile(file, fileName, url) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
fmt.Printf("Send folder %s success.\n", dirPath) |
|||
return nil |
|||
} |
|||
|
|||
// 传送文件
|
|||
func postFile(filePath string, filename string, url string) error { |
|||
payload := &bytes.Buffer{} |
|||
writer := multipart.NewWriter(payload) |
|||
|
|||
file, err := os.Open(filePath) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer file.Close() |
|||
|
|||
part, err := writer.CreateFormFile("file", filepath.ToSlash(filename)) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
fileInfo, _ := file.Stat() |
|||
bar := progressbar.DefaultBytes( |
|||
fileInfo.Size(), |
|||
fmt.Sprintf("Uploading [%s]", filename), |
|||
) |
|||
|
|||
_, err = io.Copy(io.MultiWriter(part, bar), file) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
err = writer.Close() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
req, err := http.NewRequest(http.MethodPost, url, payload) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
req.Header.Set("Content-Type", writer.FormDataContentType()) |
|||
|
|||
client := &http.Client{} |
|||
resp, err := client.Do(req) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
if resp.StatusCode != http.StatusOK { |
|||
return fmt.Errorf("upload failed with status code: %d", resp.StatusCode) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
@ -0,0 +1,233 @@ |
|||
package util |
|||
|
|||
import ( |
|||
"archive/zip" |
|||
"fmt" |
|||
"io" |
|||
"io/fs" |
|||
"os" |
|||
"path" |
|||
"path/filepath" |
|||
"strings" |
|||
) |
|||
|
|||
// 遍历目录下的文件
|
|||
func GetDirFilePaths(dirPath string, relativeOnly bool) ([]string, error) { |
|||
var filePaths []string |
|||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { |
|||
if err != nil { |
|||
return err |
|||
} |
|||
if !info.IsDir() { |
|||
if relativeOnly { |
|||
fileName := filepath.Base(path) |
|||
relativePath := filepath.ToSlash(filepath.Join(filepath.Base(dirPath), fileName)) |
|||
filePaths = append(filePaths, relativePath) |
|||
} else { |
|||
filePaths = append(filePaths, filepath.ToSlash(path)) |
|||
} |
|||
} |
|||
return nil |
|||
}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return filePaths, nil |
|||
} |
|||
|
|||
// dest:目的地址以及压缩文件名 eg:/data/logZip/2022-12-12-log.zip
|
|||
// paths:需要压缩的所有文件所组成的集合
|
|||
func CompressToZip(dest string, currentPath string, paths []string) error { |
|||
// fmt.Println("real path", currentPath)
|
|||
// 打开files 目录
|
|||
filesPath := "" |
|||
if dir, err := os.Getwd(); err == nil { |
|||
filesPath = dir + "/files/" |
|||
} |
|||
if err := os.MkdirAll(filesPath, 0755); err != nil { |
|||
fmt.Println(err.Error()) |
|||
} |
|||
// ToSlash 过滤windows的斜杠引起的bug
|
|||
zfile, err := os.Create(path.Join("./files", "/", dest)) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer zfile.Close() |
|||
zipWriter := zip.NewWriter(zfile) |
|||
defer zipWriter.Close() |
|||
|
|||
// 遍历带压缩的目录信息
|
|||
for _, src := range paths { |
|||
// 替换文件名,并且去除前后 "\" 或 "/"
|
|||
// path = strings.Trim(path, string(filepath.Separator))
|
|||
// 从配置中读取到源目录
|
|||
src = path.Join(currentPath, "/", src) |
|||
// 删除尾随路径(如果它是目录)
|
|||
src := strings.TrimSuffix(src, string(os.PathSeparator)) |
|||
err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error { |
|||
if err != nil { |
|||
return err |
|||
} |
|||
// 创建本地文件头
|
|||
header, err := zip.FileInfoHeader(info) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
// 将压缩方法设置为deflate
|
|||
header.Method = zip.Deflate |
|||
|
|||
// 在zip存档中设置文件的相对路径
|
|||
header.Name, err = filepath.Rel(filepath.Dir(src), path) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// 目录需要拼上一个 "/" ,否则会出现一个和目录一样的文件在压缩包中
|
|||
if info.IsDir() { |
|||
// header.Name += string(os.PathSeparator)
|
|||
header.Name += "/" |
|||
} else { |
|||
// 替换一下分隔符,zip不支持 "\\"
|
|||
header.Name = strings.ReplaceAll(header.Name, "\\", "/") |
|||
} |
|||
// 创建写入头的writer
|
|||
headerWriter, err := zipWriter.CreateHeader(header) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
if info.IsDir() { |
|||
return nil |
|||
} |
|||
f, err := os.Open(path) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer f.Close() |
|||
_, err = io.Copy(headerWriter, f) |
|||
return err |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
// 存到指定位置
|
|||
os.Rename(dest, path.Join("./files/", dest)) |
|||
return nil |
|||
} |
|||
|
|||
// 解压缩zip文件
|
|||
// 这个方法必须是首字母大写的,才能被注册
|
|||
// src: 需要解压的zip文件
|
|||
func DecompressZip(zpFname string) error { |
|||
// 下载文件
|
|||
archive, err := zip.OpenReader(zpFname) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
dir := filepath.Dir(zpFname) |
|||
defer archive.Close() |
|||
// 遍历目录
|
|||
for _, f := range archive.File { |
|||
filePath := filepath.Join(dir, f.Name) |
|||
if f.FileInfo().IsDir() { |
|||
os.MkdirAll(filePath, os.ModePerm) |
|||
continue |
|||
} |
|||
// 父文件夹开始闯将目录
|
|||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { |
|||
return fmt.Errorf("failed to make directory (%v)", err) |
|||
} |
|||
// 文件原有模式
|
|||
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to create file (%v)", err) |
|||
} |
|||
fileInArchive, err := f.Open() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to open file in zip (%v)", err) |
|||
} |
|||
if _, err := io.Copy(dstFile, fileInArchive); err != nil { |
|||
return fmt.Errorf("failed to copy file in zip (%v)", err) |
|||
} |
|||
dstFile.Close() |
|||
fileInArchive.Close() |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// Zip 压缩文件或目录
|
|||
// @params dst io.Writer 压缩文件可写流
|
|||
// @params src string 待压缩源文件/目录路径
|
|||
func Zip(dst io.Writer, src string) error { |
|||
// 强转一下路径
|
|||
src = filepath.Clean(src) |
|||
// 提取最后一个文件或目录的名称
|
|||
baseFile := filepath.Base(src) |
|||
// 判断src是否存在
|
|||
_, err := os.Stat(src) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// 通文件流句柄创建一个ZIP压缩包
|
|||
zw := zip.NewWriter(dst) |
|||
// 延迟关闭这个压缩包
|
|||
defer zw.Close() |
|||
|
|||
// 通过filepath封装的Walk来递归处理源路径到压缩文件中
|
|||
return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { |
|||
// 是否存在异常
|
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// 通过原始文件头信息,创建zip文件头信息
|
|||
zfh, err := zip.FileInfoHeader(info) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// 赋值默认的压缩方法,否则不压缩
|
|||
zfh.Method = zip.Deflate |
|||
|
|||
// 移除绝对路径
|
|||
tmpPath := path |
|||
index := strings.Index(tmpPath, baseFile) |
|||
if index > -1 { |
|||
tmpPath = tmpPath[index:] |
|||
} |
|||
// 替换文件名,并且去除前后 "\" 或 "/"
|
|||
tmpPath = strings.Trim(tmpPath, string(filepath.Separator)) |
|||
// 替换一下分隔符,zip不支持 "\\"
|
|||
zfh.Name = strings.ReplaceAll(tmpPath, "\\", "/") |
|||
// 目录需要拼上一个 "/" ,否则会出现一个和目录一样的文件在压缩包中
|
|||
if info.IsDir() { |
|||
zfh.Name += "/" |
|||
} |
|||
|
|||
// 写入文件头信息,并返回一个ZIP文件写入句柄
|
|||
zfw, err := zw.CreateHeader(zfh) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// 仅在他是标准文件时进行文件内容写入
|
|||
if zfh.Mode().IsRegular() { |
|||
// 打开要压缩的文件
|
|||
sfr, err := os.Open(path) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer sfr.Close() |
|||
|
|||
// 将srcFileReader拷贝到zipFilWrite中
|
|||
_, err = io.Copy(zfw, sfr) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
// 搞定
|
|||
return nil |
|||
}) |
|||
} |
|||
@ -0,0 +1,110 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net/http" |
|||
"os" |
|||
"path/filepath" |
|||
|
|||
"github.com/chyok/st/config" |
|||
"github.com/chyok/st/internal/discovery" |
|||
"github.com/chyok/st/internal/handler" |
|||
"github.com/chyok/st/web" |
|||
"github.com/urfave/cli/v2" |
|||
) |
|||
|
|||
var ( |
|||
port string |
|||
) |
|||
|
|||
// 初始化配置
|
|||
func initConfig(c *cli.Context) error { |
|||
return config.G.SetConf(port) |
|||
} |
|||
|
|||
// 接收端 管理上传
|
|||
func receiveClient() error { |
|||
//接收者角色
|
|||
go discovery.Send(discovery.Receiver) |
|||
//udp 模式监听
|
|||
go discovery.Listen(discovery.Sender, "") |
|||
|
|||
address := fmt.Sprintf("http://%s:%s", config.G.LocalIP, config.G.Port) |
|||
fmt.Printf("Server address: %s\n", address) |
|||
|
|||
http.HandleFunc("/", handler.ReceiveHandler) |
|||
http.Handle("/static/", http.StripPrefix("/static/", |
|||
http.FileServer(http.FS(web.StaticFs)))) |
|||
|
|||
fmt.Println("Waiting for transfer...") |
|||
return http.ListenAndServe(config.G.WildcardAddress, nil) |
|||
} |
|||
|
|||
// 发送端
|
|||
func sendClient() error { |
|||
// 发送者角色
|
|||
go discovery.Send(discovery.Sender) |
|||
//udp 监听指定路径下的文件
|
|||
go discovery.Listen(discovery.Receiver, config.G.FilePath) |
|||
|
|||
url := fmt.Sprintf("http://%s:%s", config.G.LocalIP, config.G.Port) |
|||
fmt.Printf("Server address: %s\n", url) |
|||
|
|||
http.HandleFunc("/", handler.SendHandler) |
|||
// 加载静态资源
|
|||
http.Handle("/static/", http.StripPrefix("/static/", |
|||
http.FileServer(http.FS(web.StaticFs)))) |
|||
http.HandleFunc("/download/", handler.FileServerHandler) |
|||
// 下载压缩包
|
|||
http.HandleFunc("/dlzip", handler.Downzip) |
|||
// udp 传送文件
|
|||
http.HandleFunc("/sendZip", handler.SendZip) |
|||
// 已经打包的zip存放位置
|
|||
http.HandleFunc("/files", handler.Dfiles) |
|||
|
|||
fmt.Println("send file to server...") |
|||
|
|||
return http.ListenAndServe(config.G.WildcardAddress, nil) |
|||
} |
|||
|
|||
// 入口函数
|
|||
func main() { |
|||
app := &cli.App{ |
|||
Name: "xtfs", |
|||
Usage: "simple file transfer tool", |
|||
UsageText: "xtfs [global options] [filename|foldername]", |
|||
Description: "xtfs is a simple command-line tool for fast local file/folder sharing", |
|||
Flags: []cli.Flag{ |
|||
&cli.StringFlag{ |
|||
Name: "port", |
|||
Value: "9099", |
|||
Usage: "server port", |
|||
Aliases: []string{"p"}, |
|||
Destination: &port, |
|||
}, |
|||
&cli.BoolFlag{ |
|||
Name: "version", |
|||
Aliases: []string{"v"}, |
|||
Usage: "print the version", |
|||
}, |
|||
}, |
|||
Action: func(c *cli.Context) error { |
|||
if c.Bool("version") { |
|||
fmt.Printf("xtfs version %s\n", config.G.Version) |
|||
return nil |
|||
} |
|||
|
|||
if c.NArg() > 0 { |
|||
currentPath := filepath.ToSlash(c.Args().Get(0)) |
|||
config.G.FilePath = currentPath |
|||
return sendClient() |
|||
} |
|||
return receiveClient() |
|||
}, |
|||
Before: initConfig, |
|||
} |
|||
err := app.Run(os.Args) |
|||
if err != nil { |
|||
fmt.Println(err.Error()) |
|||
} |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<title>File Transfer</title> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<style> |
|||
body { |
|||
font-family: Arial, sans-serif; |
|||
margin: 0; |
|||
padding: 0; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
.container { |
|||
max-width: 1000px; |
|||
margin: 0 auto; |
|||
padding: 20px; |
|||
background-color: #fff; |
|||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.center { |
|||
text-align: center; |
|||
} |
|||
|
|||
h1, h2 { |
|||
text-align: center; |
|||
color: #333; |
|||
font-size: 2rem; |
|||
} |
|||
|
|||
ul { |
|||
list-style-type: none; |
|||
padding: 0; |
|||
} |
|||
|
|||
li { |
|||
padding: 10px; |
|||
border-bottom: 1px solid #ddd; |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 1.2rem; |
|||
} |
|||
|
|||
li:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
a { |
|||
text-decoration: none; |
|||
color: #333; |
|||
} |
|||
|
|||
.btn { |
|||
display: inline-block; |
|||
padding: 8px 16px; |
|||
background-color: #007bff; |
|||
color: #fff; |
|||
border: none; |
|||
border-radius: 4px; |
|||
cursor: pointer; |
|||
margin-left: auto; |
|||
} |
|||
|
|||
.btn:hover { |
|||
background-color: #0056b3; |
|||
} |
|||
|
|||
.tips{ |
|||
display:block; |
|||
text-align:right; |
|||
font-size:0.6rem; |
|||
color:#ddd; |
|||
line-height:1.8; |
|||
margin-left:32px; |
|||
} |
|||
|
|||
.folder-icon { |
|||
width: 24px; |
|||
height: 24px; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
.file-icon { |
|||
width: 24px; |
|||
height: 24px; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
@media (max-width: 600px) { |
|||
.container { |
|||
padding: 10px; |
|||
} |
|||
} |
|||
</style> |
|||
<script type="text/javascript" src="/static/static/js/jquery.min.js"></script> |
|||
<script type="text/javascript" src="/static/static/js/bootstrap.min.js"></script> |
|||
</head> |
|||
<body> |
|||
<div class="container"> |
|||
<p>{{ .DeviceName }}</p> |
|||
<hr> |
|||
{{ if .IsDir }} |
|||
<h2>{{ .DownloadPath }}</h2> |
|||
<form action="/sendZip" method="post"> |
|||
<div class=""> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="mb-3"> |
|||
<input id="all" type="checkbox"> 全选 |
|||
</div> |
|||
<div class="mb-8"> |
|||
<div class="input-group mb-3"> |
|||
<input placeholder="目标服务器ip,端口:9080" type="input" name="serverip" class="form-control"></input> |
|||
<input type="hidden" name="curpath" value="{{ .UrlPath }}"/> |
|||
<!--button class="btn btn-primary" type="submit">下载zip</button--> |
|||
<button class="btn btn-primary" type="submit">同步选中的文件压缩包</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<ul> |
|||
{{ range .Files }} |
|||
<li> |
|||
<input class="mfile" type="checkbox" name="zipfiles" value="{{.Name}}"> |
|||
{{ if .IsDir }} |
|||
<svg class="folder-icon" viewBox="0 0 24 24"> |
|||
<path fill="currentColor" d="M10 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V8C22 6.9 21.1 6 20 6H12L10 4Z" /> |
|||
</svg> |
|||
<a href="{{ $.UrlPath }}/{{ .Name }}">{{ .Name }}</a> |
|||
{{ else }} |
|||
<svg class="file-icon" viewBox="0 0 24 24"> |
|||
<path fill="currentColor" d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M13 9V3.5L18.5 9H13Z" /> |
|||
</svg> |
|||
{{ .Name }} <span class="tips">{{.Info.Size}} {{.Info.ModTime.Format "2006-01-02 15:04:05"}} </span><a href="/download/{{ $.DownloadPath }}/{{ .Name }}" class="btn btn-primary" download>同步</a> |
|||
{{ end }} |
|||
</li> |
|||
{{ end }} |
|||
|
|||
</ul> |
|||
{{ else }} |
|||
<h2>{{ .FileName }}</h2> |
|||
<div class="center"><a href="/download/{{ .FileName }}" class="btn" download>Download</a></div> |
|||
{{ end }} |
|||
</form> |
|||
</div> |
|||
</div> |
|||
|
|||
<script> |
|||
var chkall = true; |
|||
var chknum=0; |
|||
|
|||
$(function () { |
|||
//全选按钮设置点击事件 |
|||
$("#all").click(function () { |
|||
//1、循环设置其它多选框选中状态,跟标识用的变量一样 |
|||
$(".mfile").prop("checked", chkall); |
|||
// down button toggle |
|||
if(chkall ||chknum>2){ |
|||
$("#tropt").show() |
|||
chknum +=1 |
|||
}else{ |
|||
$("#tropt").hide() |
|||
chknum -=1 |
|||
} |
|||
//2、标识的变量取反 |
|||
chkall = !chkall; |
|||
|
|||
}) |
|||
}) |
|||
|
|||
</script> |
|||
|
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,16 @@ |
|||
package web |
|||
|
|||
import "embed" |
|||
|
|||
//go:embed static
|
|||
// var CssFs embed.FS
|
|||
var StaticFs embed.FS |
|||
|
|||
//go:embed upload.tmpl
|
|||
var UploadPage string |
|||
|
|||
//go:embed download.tmpl
|
|||
var DownloadPage string |
|||
|
|||
//go:embed flist.tmpl
|
|||
var ListPage string |
|||
@ -0,0 +1,135 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<title>BI更新系统</title> |
|||
<link rel="stylesheet" href="/static/css/bootstrap.css"> |
|||
<link rel="stylesheet" href="/static/css/jquery.fileupload.css"> |
|||
<link rel="stylesheet" href="/static/css/upload.css"> |
|||
</head> |
|||
|
|||
<body> |
|||
|
|||
<div class="container"> |
|||
|
|||
<div class="page-header"> |
|||
<h1>FileUpload</h1> |
|||
</div> |
|||
|
|||
<p>Drag files or click the "Upload Files..." button to upload new files</p> |
|||
|
|||
|
|||
<div class="btn-toolbar"> |
|||
<div class="btn btn-primary fileinput-button"> |
|||
Upload files |
|||
<input id="fileupload" type="file" name="files[]" multiple> |
|||
</div> |
|||
<div class="btn btn-primary fileinput-button"> |
|||
Upload folder |
|||
<input id="folderupload" type="file" name="folders[]" directory multiple webkitdirectory> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="panel panel-default"> |
|||
<div class="panel-heading">Upload Queue</div> |
|||
<table class="table table-striped"> |
|||
<tbody id="uploads"> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="panel panel-default"> |
|||
<div class="panel-heading"> |
|||
<ol class="breadcrumb" id="path"> |
|||
<li class="active">{{.}} Recevied Files</li> |
|||
</ol> |
|||
</div> |
|||
<table class="table table-striped"> |
|||
<tbody id="listing"> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<script> |
|||
var fileInput = document.getElementById('fileupload'); |
|||
var uploads = document.getElementById('uploads'); |
|||
var listing = document.getElementById('listing'); |
|||
|
|||
fileInput.addEventListener('change', function(e) { |
|||
handleFiles(e.target.files); |
|||
}); |
|||
|
|||
document.body.addEventListener('dragover', function(e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
}, false); |
|||
|
|||
document.body.addEventListener('drop', function(e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
handleFiles(e.dataTransfer.files); |
|||
}, false); |
|||
|
|||
function handleFiles(files) { |
|||
for (var i = 0; i < files.length; i++) { |
|||
var file = files[i]; |
|||
var row = document.createElement('tr'); |
|||
row.innerHTML = '<td class="column-path"><p>' + file.name + '</p></td><td class="column-progress"><div class="progress"><div class="progress-bar" style="width: 0%;"></div></div></td>'; |
|||
uploads.appendChild(row); |
|||
uploadFile(file, row); |
|||
} |
|||
} |
|||
|
|||
function uploadFile(file, row) { |
|||
var progressBar = row.querySelector('.progress-bar'); |
|||
var formData = new FormData(); |
|||
formData.append("file", file); |
|||
var ajax = new XMLHttpRequest(); |
|||
ajax.upload.addEventListener('progress', function(e) { |
|||
var percent = Math.round((e.loaded / e.total) * 100); |
|||
progressBar.style.width = percent + '%'; |
|||
}); |
|||
ajax.addEventListener('load', function() { |
|||
uploads.removeChild(row); |
|||
var newRow = document.createElement('tr'); |
|||
newRow.innerHTML = '<td class="column-name"><p>' + file.name + '</p></td><td class="column-size"><p>' + (file.size / 1024 / 1024).toFixed(2) + ' MB</p></td>'; |
|||
listing.appendChild(newRow); |
|||
}); |
|||
ajax.open("POST", "/", true); |
|||
ajax.send(formData); |
|||
} |
|||
var folderInput = document.getElementById('folderupload'); |
|||
|
|||
folderInput.addEventListener('change', function(e) { |
|||
handleFiles(e.target.files); |
|||
}); |
|||
|
|||
function handleFiles(files) { |
|||
for (var i = 0; i < files.length; i++) { |
|||
var file = files[i]; |
|||
if (file.webkitRelativePath) { |
|||
var parts = file.webkitRelativePath.split('/'); |
|||
var fileName = parts.pop(); |
|||
var folderPath = parts.join('/'); |
|||
var row = document.createElement('tr'); |
|||
row.innerHTML = '<td class="column-path"><p>' + folderPath + '/' + fileName + '</p></td><td class="column-progress"><div class="progress"><div class="progress-bar" style="width: 0%;"></div></div></td>'; |
|||
uploads.appendChild(row); |
|||
uploadFile(file, row); |
|||
} else { |
|||
var row = document.createElement('tr'); |
|||
row.innerHTML = '<td class="column-path"><p>' + file.name + '</p></td><td class="column-progress"><div class="progress"><div class="progress-bar" style="width: 0%;"></div></div></td>'; |
|||
uploads.appendChild(row); |
|||
uploadFile(file, row); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
</body> |
|||
|
|||
</html> |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@ |
|||
@charset "UTF-8"; |
|||
/* |
|||
* jQuery File Upload Plugin CSS 1.3.0 |
|||
* https://github.com/blueimp/jQuery-File-Upload |
|||
* |
|||
* Copyright 2013, Sebastian Tschan |
|||
* https://blueimp.net |
|||
* |
|||
* Licensed under the MIT license: |
|||
* http://www.opensource.org/licenses/MIT |
|||
*/ |
|||
|
|||
.fileinput-button { |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
.fileinput-button input { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
margin: 0; |
|||
opacity: 0; |
|||
-ms-filter: 'alpha(opacity=0)'; |
|||
font-size: 200px; |
|||
direction: ltr; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
/* Fixes for IE < 8 */ |
|||
@media screen\9 { |
|||
.fileinput-button input { |
|||
filter: alpha(opacity=0); |
|||
font-size: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
.row-file { |
|||
height: 40px; |
|||
} |
|||
|
|||
.column-icon { |
|||
width: 40px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.column-name { |
|||
} |
|||
|
|||
.column-size { |
|||
width: 100px; |
|||
text-align: right; |
|||
} |
|||
|
|||
.column-move { |
|||
width: 40px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.column-delete { |
|||
width: 40px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.column-path { |
|||
} |
|||
|
|||
.column-progress { |
|||
width: 200px; |
|||
} |
|||
|
|||
.footer { |
|||
color: #999; |
|||
text-align: center; |
|||
font-size: 0.9em; |
|||
} |
|||
|
|||
#reload { |
|||
float: right; |
|||
} |
|||
|
|||
#create-input { |
|||
width: 50%; |
|||
height: 20px; |
|||
} |
|||
|
|||
#move-input { |
|||
width: 80%; |
|||
height: 20px; |
|||
} |
|||
|
|||
/* Bootstrap overrides */ |
|||
|
|||
.btn:focus { |
|||
outline: none; |
|||
} |
|||
|
|||
.btn-toolbar { |
|||
margin-top: 30px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.table .progress { |
|||
margin-top: 0px; |
|||
margin-bottom: 0px; |
|||
height: 16px; |
|||
} |
|||
|
|||
.panel-default > .panel-heading { |
|||
color: #555; |
|||
} |
|||
|
|||
.breadcrumb { |
|||
background-color: transparent; |
|||
border-radius: 0px; |
|||
margin-bottom: 0px; |
|||
padding: 0px; |
|||
} |
|||
|
|||
.breadcrumb > .active { |
|||
color: #555; |
|||
} |
|||
|
|||
.breadcrumb > li + li:before { |
|||
color: #999; |
|||
} |
|||
|
|||
.table > tbody > tr > td { |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
.table > tbody > tr > td > p { |
|||
margin: 0px; |
|||
} |
|||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,136 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<title>BI更新系统</title> |
|||
<link rel="stylesheet" href="/static/static/css/bootstrap.css"> |
|||
<link rel="stylesheet" href="/static/static/css/jquery.fileupload.css"> |
|||
<link rel="stylesheet" href="/static/static/css/upload.css"> |
|||
</head> |
|||
|
|||
<body> |
|||
|
|||
<div class="container"> |
|||
|
|||
<div class="page-header"> |
|||
<h1>FileUpload</h1> |
|||
<p>接收端角色</p> |
|||
</div> |
|||
|
|||
<p>Drag files or click the "Upload Files..." button to upload new files</p> |
|||
|
|||
|
|||
<div class="btn-toolbar"> |
|||
<div class="btn btn-primary fileinput-button"> |
|||
Upload files |
|||
<input id="fileupload" type="file" name="files[]" multiple> |
|||
</div> |
|||
<div class="btn btn-primary fileinput-button"> |
|||
Upload folder |
|||
<input id="folderupload" type="file" name="folders[]" directory multiple webkitdirectory> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="panel panel-default"> |
|||
<div class="panel-heading">Upload Queue</div> |
|||
<table class="table table-striped"> |
|||
<tbody id="uploads"> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="panel panel-default"> |
|||
<div class="panel-heading"> |
|||
<ol class="breadcrumb" id="path"> |
|||
<li class="active">{{.}} Recevied Files</li> |
|||
</ol> |
|||
</div> |
|||
<table class="table table-striped"> |
|||
<tbody id="listing"> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<script> |
|||
var fileInput = document.getElementById('fileupload'); |
|||
var uploads = document.getElementById('uploads'); |
|||
var listing = document.getElementById('listing'); |
|||
|
|||
fileInput.addEventListener('change', function(e) { |
|||
handleFiles(e.target.files); |
|||
}); |
|||
|
|||
document.body.addEventListener('dragover', function(e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
}, false); |
|||
|
|||
document.body.addEventListener('drop', function(e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
handleFiles(e.dataTransfer.files); |
|||
}, false); |
|||
|
|||
function handleFiles(files) { |
|||
for (var i = 0; i < files.length; i++) { |
|||
var file = files[i]; |
|||
var row = document.createElement('tr'); |
|||
row.innerHTML = '<td class="column-path"><p>' + file.name + '</p></td><td class="column-progress"><div class="progress"><div class="progress-bar" style="width: 0%;"></div></div></td>'; |
|||
uploads.appendChild(row); |
|||
uploadFile(file, row); |
|||
} |
|||
} |
|||
|
|||
function uploadFile(file, row) { |
|||
var progressBar = row.querySelector('.progress-bar'); |
|||
var formData = new FormData(); |
|||
formData.append("file", file); |
|||
var ajax = new XMLHttpRequest(); |
|||
ajax.upload.addEventListener('progress', function(e) { |
|||
var percent = Math.round((e.loaded / e.total) * 100); |
|||
progressBar.style.width = percent + '%'; |
|||
}); |
|||
ajax.addEventListener('load', function() { |
|||
uploads.removeChild(row); |
|||
var newRow = document.createElement('tr'); |
|||
newRow.innerHTML = '<td class="column-name"><p>' + file.name + '</p></td><td class="column-size"><p>' + (file.size / 1024 / 1024).toFixed(2) + ' MB</p></td>'; |
|||
listing.appendChild(newRow); |
|||
}); |
|||
ajax.open("POST", "/", true); |
|||
ajax.send(formData); |
|||
} |
|||
var folderInput = document.getElementById('folderupload'); |
|||
|
|||
folderInput.addEventListener('change', function(e) { |
|||
handleFiles(e.target.files); |
|||
}); |
|||
|
|||
function handleFiles(files) { |
|||
for (var i = 0; i < files.length; i++) { |
|||
var file = files[i]; |
|||
if (file.webkitRelativePath) { |
|||
var parts = file.webkitRelativePath.split('/'); |
|||
var fileName = parts.pop(); |
|||
var folderPath = parts.join('/'); |
|||
var row = document.createElement('tr'); |
|||
row.innerHTML = '<td class="column-path"><p>' + folderPath + '/' + fileName + '</p></td><td class="column-progress"><div class="progress"><div class="progress-bar" style="width: 0%;"></div></div></td>'; |
|||
uploads.appendChild(row); |
|||
uploadFile(file, row); |
|||
} else { |
|||
var row = document.createElement('tr'); |
|||
row.innerHTML = '<td class="column-path"><p>' + file.name + '</p></td><td class="column-progress"><div class="progress"><div class="progress-bar" style="width: 0%;"></div></div></td>'; |
|||
uploads.appendChild(row); |
|||
uploadFile(file, row); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
</body> |
|||
|
|||
</html> |
|||
Loading…
Reference in new issue