Browse Source

go版本文件同步

master
453530270@qq.com 2 years ago
parent
commit
3ca9c07bcc
  1. 146
      xgfs/.gitignore
  2. 21
      xgfs/LICENSE
  3. 26
      xgfs/Makefile
  4. 62
      xgfs/README.md
  5. 73
      xgfs/config/config.go
  6. 19
      xgfs/go.mod
  7. 33
      xgfs/go.sum
  8. 104
      xgfs/internal/discovery/discovery.go
  9. 351
      xgfs/internal/handler/handler.go
  10. 88
      xgfs/internal/transfer/receive.go
  11. 118
      xgfs/internal/transfer/send.go
  12. 233
      xgfs/internal/util/util.go
  13. 110
      xgfs/main.go
  14. 177
      xgfs/web/download.tmpl
  15. 16
      xgfs/web/embed.go
  16. 135
      xgfs/web/flist.tmpl
  17. 1
      xgfs/web/static/css/bootstrap.css
  18. 36
      xgfs/web/static/css/jquery.fileupload.css
  19. 97
      xgfs/web/static/css/upload.css
  20. 54
      xgfs/web/static/images/folder.svg
  21. 1
      xgfs/web/static/images/php.svg
  22. 63
      xgfs/web/static/images/txt.svg
  23. BIN
      xgfs/web/static/images/zip.png
  24. 7
      xgfs/web/static/js/bootstrap.min.js
  25. 4
      xgfs/web/static/js/jquery-2.1.3.min.js
  26. 4
      xgfs/web/static/js/jquery.min.js
  27. 136
      xgfs/web/upload.tmpl

146
xgfs/.gitignore

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

21
xgfs/LICENSE

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

26
xgfs/Makefile

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

62
xgfs/README.md

@ -0,0 +1,62 @@
# Simple Transfer
![GitHub tag (with filter)](https://img.shields.io/github/v/tag/chyok/st)
![GitHub License](https://img.shields.io/github/license/chyok/st)
`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.
![example](https://github.com/chyok/st/assets/32629225/a638b0d2-f509-4e34-a99b-9f9e2a757e02)
## 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).

73
xgfs/config/config.go

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

19
xgfs/go.mod

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

33
xgfs/go.sum

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

104
xgfs/internal/discovery/discovery.go

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

351
xgfs/internal/handler/handler.go

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

88
xgfs/internal/transfer/receive.go

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

118
xgfs/internal/transfer/send.go

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

233
xgfs/internal/util/util.go

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

110
xgfs/main.go

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

177
xgfs/web/download.tmpl

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

16
xgfs/web/embed.go

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

135
xgfs/web/flist.tmpl

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

1
xgfs/web/static/css/bootstrap.css

File diff suppressed because one or more lines are too long

36
xgfs/web/static/css/jquery.fileupload.css

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

97
xgfs/web/static/css/upload.css

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

54
xgfs/web/static/images/folder.svg

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="60.001px" viewBox="0 0 64 60.001" style="enable-background:new 0 0 64 60.001;" xml:space="preserve">
<g id="Folder">
<g>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:#CCA352;" d="M60,4.001H24C24,1.792,22.209,0,20,0H4
C1.791,0,0,1.792,0,4.001V8v6.001v2c0,2.209,1.791,4,4,4h56c2.209,0,4-1.791,4-4V8C64,5.791,62.209,4.001,60,4.001z"/>
</g>
</g>
<g id="File_1_">
<g>
<path style="fill:#FFFFFF;" d="M56,8H8c-2.209,0-4,1.791-4,4.001v4c0,2.209,1.791,4,4,4h48c2.209,0,4-1.791,4-4v-4
C60,9.791,58.209,8,56,8z"/>
</g>
</g>
<g id="Folder_1_">
<g>
<path style="fill:#FFCC66;" d="M60,12.001H4c-2.209,0-4,1.791-4,4v40c0,2.209,1.791,4,4,4h56c2.209,0,4-1.791,4-4v-40
C64,13.792,62.209,12.001,60,12.001z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
xgfs/web/static/images/php.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

63
xgfs/web/static/images/txt.svg

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 56 56" style="enable-background:new 0 0 56 56;" xml:space="preserve">
<g>
<path style="fill:#E9E9E0;" d="M36.985,0H7.963C7.155,0,6.5,0.655,6.5,1.926V55c0,0.345,0.655,1,1.463,1h40.074
c0.808,0,1.463-0.655,1.463-1V12.978c0-0.696-0.093-0.92-0.257-1.085L37.607,0.257C37.442,0.093,37.218,0,36.985,0z"/>
<polygon style="fill:#D9D7CA;" points="37.5,0.151 37.5,12 49.349,12 "/>
<path style="fill:#95A5A5;" d="M48.037,56H7.963C7.155,56,6.5,55.345,6.5,54.537V39h43v15.537C49.5,55.345,48.845,56,48.037,56z"/>
<g>
<path style="fill:#FFFFFF;" d="M21.867,42.924v1.121h-3.008V53h-1.654v-8.955h-3.008v-1.121H21.867z"/>
<path style="fill:#FFFFFF;" d="M28.443,48.105L31,53h-1.9l-1.6-3.801h-0.137L25.641,53h-1.9l2.557-4.895l-2.721-5.182h1.873
l1.777,4.102h0.137l1.928-4.102h1.873L28.443,48.105z"/>
<path style="fill:#FFFFFF;" d="M40.529,42.924v1.121h-3.008V53h-1.654v-8.955h-3.008v-1.121H40.529z"/>
</g>
<path style="fill:#C8BDB8;" d="M18.5,13h-6c-0.553,0-1-0.448-1-1s0.447-1,1-1h6c0.553,0,1,0.448,1,1S19.053,13,18.5,13z"/>
<path style="fill:#C8BDB8;" d="M21.5,18h-9c-0.553,0-1-0.448-1-1s0.447-1,1-1h9c0.553,0,1,0.448,1,1S22.053,18,21.5,18z"/>
<path style="fill:#C8BDB8;" d="M25.5,18c-0.26,0-0.521-0.11-0.71-0.29c-0.181-0.19-0.29-0.44-0.29-0.71s0.109-0.52,0.3-0.71
c0.36-0.37,1.04-0.37,1.41,0c0.18,0.19,0.29,0.45,0.29,0.71c0,0.26-0.11,0.52-0.29,0.71C26.02,17.89,25.76,18,25.5,18z"/>
<path style="fill:#C8BDB8;" d="M37.5,18h-8c-0.553,0-1-0.448-1-1s0.447-1,1-1h8c0.553,0,1,0.448,1,1S38.053,18,37.5,18z"/>
<path style="fill:#C8BDB8;" d="M12.5,33c-0.26,0-0.521-0.11-0.71-0.29c-0.181-0.19-0.29-0.45-0.29-0.71
c0-0.26,0.109-0.52,0.29-0.71c0.37-0.37,1.05-0.37,1.42,0.01c0.18,0.18,0.29,0.44,0.29,0.7c0,0.26-0.11,0.52-0.29,0.71
C13.02,32.89,12.76,33,12.5,33z"/>
<path style="fill:#C8BDB8;" d="M24.5,33h-8c-0.553,0-1-0.448-1-1s0.447-1,1-1h8c0.553,0,1,0.448,1,1S25.053,33,24.5,33z"/>
<path style="fill:#C8BDB8;" d="M43.5,18h-2c-0.553,0-1-0.448-1-1s0.447-1,1-1h2c0.553,0,1,0.448,1,1S44.053,18,43.5,18z"/>
<path style="fill:#C8BDB8;" d="M34.5,23h-22c-0.553,0-1-0.448-1-1s0.447-1,1-1h22c0.553,0,1,0.448,1,1S35.053,23,34.5,23z"/>
<path style="fill:#C8BDB8;" d="M43.5,23h-6c-0.553,0-1-0.448-1-1s0.447-1,1-1h6c0.553,0,1,0.448,1,1S44.053,23,43.5,23z"/>
<path style="fill:#C8BDB8;" d="M16.5,28h-4c-0.553,0-1-0.448-1-1s0.447-1,1-1h4c0.553,0,1,0.448,1,1S17.053,28,16.5,28z"/>
<path style="fill:#C8BDB8;" d="M30.5,28h-10c-0.553,0-1-0.448-1-1s0.447-1,1-1h10c0.553,0,1,0.448,1,1S31.053,28,30.5,28z"/>
<path style="fill:#C8BDB8;" d="M43.5,28h-9c-0.553,0-1-0.448-1-1s0.447-1,1-1h9c0.553,0,1,0.448,1,1S44.053,28,43.5,28z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
xgfs/web/static/images/zip.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

7
xgfs/web/static/js/bootstrap.min.js

File diff suppressed because one or more lines are too long

4
xgfs/web/static/js/jquery-2.1.3.min.js

File diff suppressed because one or more lines are too long

4
xgfs/web/static/js/jquery.min.js

File diff suppressed because one or more lines are too long

136
xgfs/web/upload.tmpl

@ -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…
Cancel
Save