Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
imacks committed Oct 1, 2020
1 parent fcbfffe commit 31fb47f
Show file tree
Hide file tree
Showing 8 changed files with 872 additions and 2 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
# cowtransfer
Client library for cowtransfer...shhh
cowtransfer
===========
This is a client for https://cowtransfer.cn

Most code is from https://github.com/Mikubill/cowtransfer-uploader. I have removed dependency on
`github.com/cheggaaa/pb` in favor of various progress event hooks. The intention is to decouple
the CLI from the library API.
103 changes: 103 additions & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"flag"
"fmt"
"os"
"time"
"strings"

"github.com/imacks/cowtransfer"
)

var (
token string
password string
parallel int
blksize int
timeout int
verbose bool
verify bool
maxRetry int
)

func init() {
flag.StringVar(&token, "c", "", "Cookie token (optional)")
flag.StringVar(&password, "p", "", "Password (optional)")
flag.BoolVar(&verbose, "v", false, "Verbose mode")
flag.BoolVar(&verify, "V", false, "Verify by hash")
flag.IntVar(&timeout, "T", 10, "Timeout in seconds")
flag.IntVar(&blksize, "b", 262144, "Block size for uploading")
flag.IntVar(&parallel, "l", 4, "Parallel threads")
flag.IntVar(&maxRetry, "r", 3, "Maximum retries")

flag.Usage = func() {
fmt.Fprintf(os.Stdout, "%s %s () %s\n", "cowtransfer", "1.0.0", "cowtransfer.cn client")
fmt.Fprintln(os.Stdout, "")
fmt.Fprintf(os.Stdout, "Usage: %s [<optional-params>] file1 file2... \n", os.Args[0])
fmt.Fprintf(os.Stdout, " %s [<optional-params>] url\n", os.Args[0])
fmt.Fprintln(os.Stdout, "")
fmt.Fprintln(os.Stdout, "Parameters:")
flag.PrintDefaults()
}
flag.Parse()
}

func main() {
if blksize > 4194304 {
fmt.Printf("WARNING! Block size too large\n")
blksize = 524288
}

files := flag.Args()
if len(files) == 0 {
fmt.Printf("FATAL! files not specified\n")
os.Exit(1)
}

cow := cowtransfer.NewCowClient()
cow.BlockSize = blksize
cow.DownloadParallel = parallel
cow.UploadParallel = parallel
cow.MaxRetry = maxRetry
cow.Password = password
cow.Timeout = time.Duration(timeout) * time.Second
cow.Token = token
cow.VerifyHash = verify

if len(files) == 1 && strings.HasPrefix(files[0], "https://") {
fi, err := cow.Files(files[0])
if err != nil {
panic(err)
}

for i, v := range fi {
fmt.Fprintf(os.Stdout, "idx: %d\n", i)
fmt.Fprintf(os.Stdout, "file: %s\n", v.FileName)
fmt.Fprintf(os.Stdout, "url: %s\n", v.URL)
fmt.Fprintf(os.Stdout, "size: %d\n", v.Size)
if v.Error != nil {
fmt.Fprintf(os.Stdout, "err: %s\n", v.Error.Error())
}

fmt.Fprintf(os.Stdout, "\n")
}

return
}

var f []string
for _, v := range files {
if strings.HasPrefix(v, "https://") {
fmt.Printf("FATAL! upload supports local files only\n")
os.Exit(1)
}
f = append(f, v)
}

dl, err := cow.Upload(f)
if err != nil {
panic(err)
}
fmt.Fprintf(os.Stdout, "link: %s\n", dl)
}
73 changes: 73 additions & 0 deletions cowclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cowtransfer

import (
"time"
"os"
"fmt"
)

const (
defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36 Edg/85.0.564.51"
defaultBlockSize = 4194304
)

// ProgressHook is a hook for upload and download progress
type ProgressHook struct {
Start func(fi os.FileInfo)
Finish func(fi os.FileInfo)
Seal func(fi os.FileInfo)
Block func(item int64, current, max int)
Retry func(op string, count int, err error)
RetryBlock func(op string, item int64, block int, err error)
}

// CowClient is a client for CowTransfer.cn
type CowClient struct {
Timeout time.Duration
MaxRetry int
UserAgent string
Progress ProgressHook
VerifyHash bool
Password string
UploadParallel int
DownloadParallel int
Token string
BlockSize int
}

// NewCowClient creates a CowClient using default parameters
func NewCowClient() *CowClient {
progHook := ProgressHook{
Start: func(fi os.FileInfo) {
fmt.Printf("[start] file %s | size %d\n", fi.Name(), fi.Size())
},
Finish: func(fi os.FileInfo) {
fmt.Printf("[finish] file %s | size %d\n", fi.Name(), fi.Size())
},
Seal: func(fi os.FileInfo) {
fmt.Printf("[seal] file %s | size %d\n", fi.Name(), fi.Size())
},
Block: func(item int64, current, max int) {
fmt.Printf("[block] item %d | current %d | len %d\n", item, current, max)
},
RetryBlock: func(op string, item int64, block int, err error) {
fmt.Printf("[blkerror] %s | item %d | block %d | %v\n", op, item, block, err)
},
Retry: func(op string, count int, err error) {
fmt.Printf("[error] retry %s | attempt %d | %v\n", op, count, err)
},
}

return &CowClient{
UserAgent: defaultUA,
BlockSize: defaultBlockSize,
UploadParallel: 4,
DownloadParallel: 4,
Timeout: time.Duration(10) * time.Second,
Token: "",
Password: "",
VerifyHash: true,
MaxRetry: 3,
Progress: progHook,
}
}
140 changes: 140 additions & 0 deletions download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package cowtransfer

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"time"
"strconv"
)

const (
downloadDetailsURL = "https://cowtransfer.com/transfer/transferdetail?url=%s&treceive=undefined&passcode=%s"
downloadConfigURL = "https://cowtransfer.com/transfer/download?guid=%s"
)

var regex = regexp.MustCompile("[0-9a-f]{14}")

// CowFileInfo represents information about a file
type CowFileInfo struct {
FileName string `json:"fileName"`
Size int64 `json:"size"`
URL string `json:"url"`
Error error `json:"error"`
}

type downloadDetailsResponse struct {
GUID string `json:"guid"`
DownloadName string `json:"downloadName"`
Deleted bool `json:"deleted"`
Uploaded bool `json:"uploaded"`
Details []downloadDetailsBlock `json:"transferFileDtos"`
}

type downloadDetailsBlock struct {
GUID string `json:"guid"`
FileName string `json:"fileName"`
Size string `json:"size"`
}

type downloadConfigResponse struct {
Link string `json:"link"`
}

// Files return information on all files in a download link
func (cc *CowClient) Files(link string) ([]CowFileInfo, error) {
fileID := regex.FindString(link)
if fileID == "" {
return nil, fmt.Errorf("unknown download code format")
}

detailsURL := fmt.Sprintf(downloadDetailsURL, fileID, cc.Password)
response, err := http.Get(detailsURL)
if err != nil {
return nil, err
}

req, err := http.NewRequest("GET", detailsURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Referer", fmt.Sprintf("https://cowtransfer.com/s/%s", fileID))
req.Header.Set("Cookie", fmt.Sprintf("cf-cs-k-20181214=%d;", time.Now().UnixNano()))
response, err = http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
_ = response.Body.Close()

details := new(downloadDetailsResponse)
if err := json.Unmarshal(body, details); err != nil {
fmt.Printf("fsd %s\n", string(body))
return nil, err
}

if details.GUID == "" {
return nil, fmt.Errorf("link invalid")
} else if details.Deleted {
return nil, fmt.Errorf("link deleted")
} else if !details.Uploaded {
return nil, fmt.Errorf("link not finish upload yet")
}

result := []CowFileInfo{}
for _, item := range details.Details {
cowfi := cc.getItemDownloadURL(item)
result = append(result, cowfi)
}

return result, nil
}

func (cc *CowClient) getItemDownloadURL(item downloadDetailsBlock) CowFileInfo {
result := CowFileInfo{
FileName: item.FileName,
}

configURL := fmt.Sprintf(downloadConfigURL, item.GUID)
req, err := http.NewRequest("POST", configURL, nil)
if err != nil {
result.Error = err
return result
}

response, err := http.DefaultClient.Do(addHeaders(req, cc.UserAgent))
if err != nil {
result.Error = err
return result
}

body, err := ioutil.ReadAll(response.Body)
if err != nil {
result.Error = err
return result
}
_ = response.Body.Close()

config := new(downloadConfigResponse)
if err := json.Unmarshal(body, config); err != nil {
result.Error = err
return result
}
result.URL = config.Link

numSize, err := strconv.ParseFloat(item.Size, 10)
if err != nil {
result.Error = err
result.Size = 0
return result
}

result.Size = int64(numSize * 1024)
return result
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/imacks/cowtransfer

go 1.14

require (
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
33 changes: 33 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cowtransfer

import (
"os"
)

func isExist(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
if os.IsNotExist(err) {
return false
}

return false
}

return true
}

func isDir(path string) bool {
s, err := os.Stat(path)
if err != nil {
return false
}
return s.IsDir()
}

func isFile(path string) bool {
return !isDir(path)
}
Loading

0 comments on commit 31fb47f

Please sign in to comment.