Last active
May 27, 2018 21:30
-
-
Save xiconet/b6c6c3ad26dd65d017f2abc51ab1038b to your computer and use it in GitHub Desktop.
Hightail (formerly Yousendit) basic console client in golang
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"fmt" | |
"github.com/alexflint/go-arg" | |
"github.com/cheggaaa/pb" | |
"github.com/nu7hatch/gouuid" | |
"github.com/xiconet/utils" | |
"io" | |
"io/ioutil" | |
"math" | |
"mime/multipart" | |
"net/http" | |
"net/url" | |
"os" | |
ospath "path/filepath" | |
"sort" | |
"strconv" | |
"strings" | |
"time" | |
"menteslibres.net/gosexy/to" | |
"menteslibres.net/gosexy/yaml" | |
) | |
const ( | |
host = "spaces.hightail.com" | |
cfg_file = "C:/Users/ARC/.config/hightail.yml" | |
) | |
var ( | |
auth_url = fmt.Sprintf("https://api.%s/api/v1/auth", host) | |
base_url = fmt.Sprintf("https://folders.%s", host) | |
download_url = fmt.Sprintf("https://download.%s/api/v1/download", host) | |
upload_url = fmt.Sprintf("https://upload.%s/api/v1/upload", host) | |
cookies = map[string]string{} | |
) | |
type LoginData struct { | |
SessionID string `json:"sessionId"` | |
User struct { | |
Id string `json:"id"` | |
Email string `json "email"` | |
Name string `json:"name"` | |
Verified bool `json:"verified"` | |
IsNative bool `json:"isNative"` | |
Status string `json:"status"` | |
InletType string `json:"inletType"` | |
AuthType string `json:"authType"` | |
OrganizationId string `json:"organizationId"` | |
EarlyAccess bool `json:"earlyAccess"` | |
UpdatedAt int `json:"updatedAt"` | |
CreatedAt int `json:"createdAt"` | |
Active bool `json:"active"` | |
Native bool `json:"native"` | |
} `json:"user"` | |
} | |
type Node struct { | |
CreateDate string `json:"createDate"` | |
EffectivePermissions []string `json:"effectivePermissions"` | |
FolderType string `json:"folderType"` | |
Id string `json:"id"` | |
IsDeleted bool `json:"isDeleted"` | |
IsDirectory bool `json:"isDirectory"` | |
ModifyDate string `json:"modifyDate"` | |
Name string `json:"name"` | |
OwnerId string `json:"ownerId"` | |
PhysicalFileSize string `json:"physicalFileSize"` | |
ParentId string `json:"parentId"` | |
Revision string `json:"revision"` | |
} | |
type Folder struct { | |
Children []Node `json:"children"` | |
Id string `json:"id"` | |
} | |
type ByName []Node | |
func (a ByName) Len() int { return len(a) } | |
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } | |
func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } | |
type NewDir struct { | |
GeneratedFileId string `json:"generatedFileId"` | |
IsDeleted bool `json:"isDeleted"` | |
Revision string `json:"revision"` | |
} | |
type UpResp struct { | |
ClientCreatedDate string `json:"clientCreatedDate"` | |
ClientUpdatedDate string `json:"clientUpdatedDate"` | |
CreatedDate string `json:"createdDate"` | |
CreatorId string `json:"creatorId"` | |
FolderId string `json:"folderId"` | |
Id string `json:"id"` | |
LogicalFileId string `json:"logicalFileId"` | |
Name string `json:"name"` | |
Region string `json:"region"` | |
Revision string `json:"revision"` | |
RevisionStr string `json:"revisionStr"` | |
Size string `json:"size"` | |
UfId string `json:"ufId"` | |
UpdatedDate string `json:"updatedDate"` | |
} | |
type ReqData struct { | |
Directory bool `json:"directory"` | |
Id string `json:"id,omitempty"` | |
Name string `json:"name,omitempty"` | |
NewFilename string `json:"newFilename,omitempty"` | |
ParentId string `json:"parentId,omitempty"` | |
} | |
func userCfg(user string) (email, pwd string) { | |
cfg, err := yaml.Open(cfg_file) | |
if err != nil { | |
panic(err) | |
} | |
if user == "current_user" { | |
user = to.String(cfg.Get("users", "current_user")) | |
} | |
pwd = to.String(cfg.Get(user, "password")) | |
email = to.String(cfg.Get(user, "email")) | |
return | |
} | |
func apiReq(method, uri string, data interface{}) (error, string) { | |
var req *http.Request | |
var err error | |
if method == "GET" { | |
req, err = http.NewRequest(method, uri, nil) | |
if err != nil { | |
panic(err) | |
} | |
} | |
if data != nil { | |
js, err := json.Marshal(data) | |
if err != nil { | |
panic(err) | |
} | |
req, err = http.NewRequest(method, uri, strings.NewReader(string(js))) | |
if err != nil { | |
panic(err) | |
} | |
req.Header.Set("Content-Type", "application/json") | |
} | |
if req == nil { | |
req, err = http.NewRequest(method, uri, nil) | |
if err != nil { | |
panic(err) | |
} | |
} | |
if !(strings.Contains(uri, "login")) { | |
for k, v := range cookies { | |
req.Header.Add("Cookie", fmt.Sprintf("%s=%s", k, v)) | |
} | |
} | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
panic(err) | |
} | |
defer resp.Body.Close() | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
panic(err) | |
} | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status", resp.Status) | |
fmt.Println(string(body)) | |
} | |
return nil, string(body) | |
} | |
func login(email, pwd string) { | |
data := map[string]string{"email": email, "password": pwd} | |
uri, _ := url.Parse(auth_url + "/login") | |
err, body := apiReq("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println("login request error:", err) | |
os.Exit(1) | |
} | |
ld := LoginData{} | |
err = json.Unmarshal([]byte(body), &ld) | |
if err != nil { | |
fmt.Println("json error:", err) | |
fmt.Println(body) | |
os.Exit(1) | |
} | |
cookies["sessionId"] = ld.SessionID | |
cookies["userId"] = ld.User.Id | |
} | |
func listFolder(folderId string) (f Folder) { | |
uri, _ := url.Parse(base_url) | |
uri.Path += fmt.Sprintf("/api/v1/hfsEdge/children/%s", folderId) | |
err, body := apiReq("GET", uri.String(), nil) | |
if err != nil { | |
fmt.Println("apiReq error:", err) | |
fmt.Println(body) | |
return | |
} | |
err = json.Unmarshal([]byte(body), &f) | |
if err != nil { | |
fmt.Println("json error:", err) | |
fmt.Println("body:", body) | |
os.Exit(1) | |
} | |
return | |
} | |
func treeList(folderId string, i, depth int) { | |
idt := strings.Repeat(" ", 2*i) | |
node := listFolder(folderId) | |
sort.Sort(ByName(node.Children)) | |
for _, c := range node.Children { | |
if !c.IsDirectory { | |
pfs, _ := strconv.Atoi(c.PhysicalFileSize) | |
fsize, _ := utils.NiceBytes(int64(pfs)) | |
sfmt := fmt.Sprintf("%%s%%-%ds %%10s\n", 67-2*i) | |
fmt.Printf(sfmt, idt, c.Name, fsize) | |
} else if depth == 0 || i < depth { | |
fmt.Printf("%s%s/\n", idt, c.Name) | |
treeList(c.Id, i+1, depth) | |
} | |
} | |
} | |
func treeSize(folderID string, usedSpace int64) int64 { | |
node := listFolder(folderID) | |
for _, c := range node.Children { | |
if !c.IsDirectory { | |
pfs, _ := strconv.Atoi(c.PhysicalFileSize) | |
usedSpace += int64(pfs) | |
} else { | |
usedSpace = treeSize(c.Id, usedSpace) | |
} | |
} | |
return usedSpace | |
} | |
func pathToID(path, rootID string) (found bool, n Node) { | |
if path == "/" { | |
return true, Node{Id: "0", IsDirectory: true} | |
} | |
for _, p := range strings.Split(path, "/") { | |
found = false | |
children := listFolder(rootID).Children | |
for _, c := range children { | |
if c.Name == p { | |
found = true | |
rootID = c.Id | |
n = c | |
break | |
} | |
} | |
if !found { | |
fmt.Println("error: path not found:", p) | |
} | |
} | |
return | |
} | |
func createFolder(folder_name, parent_id string) (nd NewDir) { | |
uri, _ := url.Parse(base_url + "/api/v1/hfsEdge/create") | |
data := ReqData{Name: folder_name, Directory: true, ParentId: parent_id} | |
err, body := apiReq("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println("error:", err) | |
} | |
err = json.Unmarshal([]byte(body), &nd) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Println(body) | |
} | |
return | |
} | |
func download(n Node, dest string) int64 { | |
if dest == "" { | |
dest = n.Name | |
} | |
uri, _ := url.Parse(download_url) | |
uri.Path += fmt.Sprintf("/link/HIGHTAIL_FILE/%s/%s/%s", n.Id, n.Revision, n.Name) | |
fmt.Println("url:", uri.String()) | |
req, err := http.NewRequest("GET", uri.String(), nil) | |
if err != nil { | |
panic(err) | |
} | |
for k, v := range cookies { | |
req.Header.Add("Cookie", fmt.Sprintf("%s=%s", k, v)) | |
} | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
fmt.Println("request error:", err) | |
fmt.Printf("request: %+v\n", req) | |
os.Exit(1) | |
} | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status:", resp.Status) | |
os.Exit(1) | |
} | |
defer resp.Body.Close() | |
out, err := os.Create(dest) | |
if err != nil { | |
panic(err) | |
} | |
defer out.Close() | |
var srcSize, fl int64 | |
cl := resp.Header.Get("Content-Length") | |
if cl != "" { | |
i, err := strconv.Atoi(cl) | |
if err != nil { | |
fmt.Println(err) | |
} | |
srcSize = int64(i) | |
} else { | |
pfs, err := strconv.Atoi(n.PhysicalFileSize) | |
if err != nil { | |
fmt.Println(err) | |
} | |
fl = int64(pfs) | |
srcSize = fl | |
} | |
src := resp.Body | |
bar := pb.New(int(srcSize)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10) | |
if srcSize >= 1024*1024 { | |
bar.ShowSpeed = true | |
} | |
bar.Start() | |
writer := io.MultiWriter(out, bar) | |
m, err := io.Copy(writer, src) | |
if err != nil { | |
fmt.Println("Error while downloading from:", uri.String(), "-", err) | |
return 0 | |
} | |
bar.Finish() | |
return m | |
} | |
func downsync(obj Node, localPath string, tBytes int64) int64 { | |
err := os.MkdirAll(localPath, 0777) | |
if err != nil { | |
fmt.Println("error: could not create new folder", localPath, ":", err) | |
os.Exit(1) | |
} | |
for _, c := range listFolder(obj.Id).Children { | |
if !c.IsDirectory { | |
tBytes += download(c, ospath.Join(localPath, c.Name)) | |
} else { | |
tBytes = downsync(c, ospath.Join(localPath, c.Name), tBytes) | |
} | |
} | |
return tBytes | |
} | |
//func GetFileContentType tries to detect a file's mime type | |
func GetFileContentType(out *os.File) (string, error) { | |
// Only the first 512 bytes are used to sniff the content type. | |
buffer := make([]byte, 512) | |
_, err := out.Read(buffer) | |
if err != nil { | |
return "", err | |
} | |
// The net/http DectectContentType function returns a valid | |
// "application/octet-stream" if no other match is found. | |
contentType := http.DetectContentType(buffer) | |
return contentType, nil | |
} | |
func makeChunk(fh *os.File, offset int64) []byte { | |
chunksize := int64(5 * 1024 * 1024) | |
p := make([]byte, chunksize) | |
//fmt.Println("offset:", offset) | |
n, _ := fh.ReadAt(p, offset) | |
return p[:n] | |
} | |
func uploadRequest(uri string, params interface{}, chunk []byte) (*http.Request, error) { | |
body := &bytes.Buffer{} | |
ck := bytes.NewBuffer(chunk) | |
writer := multipart.NewWriter(body) | |
part, err := writer.CreateFormFile("file", "blob") | |
if err != nil { | |
return nil, err | |
} | |
_, err = io.Copy(part, ck) | |
for key, val := range params.(map[string]string) { | |
_ = writer.WriteField(key, val) | |
} | |
err = writer.Close() | |
if err != nil { | |
return nil, err | |
} | |
req, err := http.NewRequest("POST", uri, body) | |
if err != nil { | |
panic(err) | |
} | |
for k, v := range cookies { | |
req.Header.Add("Cookie", fmt.Sprintf("%s=%s", k, v)) | |
} | |
req.Header.Add("Content-Type", writer.FormDataContentType()) | |
return req, nil | |
} | |
func resumableUpload(fp string, parent Node) (uresp UpResp) { | |
const MCS = int64(5 * 1024 * 1024) // max. chunk size | |
mcs := fmt.Sprintf("%d", MCS) | |
fn := ospath.Base(fp) | |
uri, err := url.Parse(upload_url) | |
uri.Path += fmt.Sprintf("/folder/resumable/%s", parent.Id) | |
if err != nil { | |
panic(err) | |
} | |
fh, err := os.Open(fp) | |
if err != nil { | |
panic(err) | |
} | |
defer fh.Close() | |
stat, err := fh.Stat() | |
if err != nil { | |
panic(err) | |
} | |
fsize := stat.Size() | |
filesize := fmt.Sprintf("%d", fsize) | |
chunks := math.Ceil(float64(fsize) / float64(MCS)) | |
tchunks := fmt.Sprintf("%d", chunks) | |
rts := fmt.Sprintf("%d", int64(math.Min(float64(fsize), float64(MCS)))) | |
rt, err := GetFileContentType(fh) | |
if err != nil { | |
fmt.Println(err) | |
os.Exit(1) | |
} | |
uid, err := uuid.NewV4() | |
if err != nil { | |
panic(err) | |
} | |
rin := uid.String() | |
chunk_number := 1 | |
data := map[string]string{ | |
"resumableChunkNumber": "1", | |
"resumableChunkSize": mcs, | |
"resumableCurrentChunkSize": rts, | |
"resumableTotalSize": filesize, | |
"resumableType": rt, | |
"resumableIdentifier": rin, | |
"resumableFilename": fn, | |
"resumableRelativePath": fn, | |
"resumableTotalChunks": tchunks, | |
} | |
offset := int64(0) | |
for offset < fsize { | |
chunk := makeChunk(fh, offset) | |
data["resumableCurrentChunkSize"] = fmt.Sprintf("%d", len(chunk)) | |
req, err := uploadRequest(uri.String(), data, chunk) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Printf("uploading chunk #%s (%d bytes)...\n", data["resumableChunkNumber"], len(chunk)) | |
resp, err := http.DefaultClient.Do(req) | |
if err != nil { | |
fmt.Println("upload request error:", err) | |
return | |
} | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
fmt.Println("read body error:", err) | |
return | |
} | |
fmt.Println("server status:", resp.Status) | |
if resp.StatusCode != 200 { | |
fmt.Println("error: bad server status:", resp.Status) | |
fmt.Println("reponse body:", string(body)) | |
return | |
} | |
err = json.Unmarshal(body, &uresp) | |
if err != nil { | |
fmt.Println(err) | |
fmt.Println("reponse body:", string(body)) | |
return | |
} | |
chunk_number += 1 | |
data["resumableChunkNumber"] = fmt.Sprintf("%d", chunk_number) | |
offset += int64(len(chunk)) | |
} | |
return | |
} | |
func rename(n Node, newname string) string { | |
uri, _ := url.Parse(base_url) | |
uri.Path += "/api/v1/hfsEdge/rename" | |
data := ReqData{Id: n.Id, NewFilename: newname, Directory: n.IsDirectory} | |
err, body := apiReq("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println(err) | |
} | |
return body | |
} | |
func deleteNode(n Node) string { | |
uri, _ := url.Parse(base_url) | |
uri.Path += fmt.Sprintf("/api/v1/hfsEdge/delete") | |
data := ReqData{Id: n.Id, Directory: n.IsDirectory} | |
err, body := apiReq("POST", uri.String(), data) | |
if err != nil { | |
fmt.Println(err) | |
} | |
return body | |
} | |
func main() { | |
var args struct { | |
Path string `arg:"positional,help:node path"` | |
User string `arg:"-u,help:account user name"` | |
Tree bool `arg:"-t,help:recursive (tree-style) folder list"` | |
Recurse int `arg:"-R,help:recursion depth"` | |
Info bool `arg:"-i,help:compute total size for the specified folder"` | |
Mkdir string `arg:"-m,help:create a new folder in the specified parent folder path"` | |
Upload string `arg:"-p,help:upload a file to the specified parent folder path"` | |
Download bool `arg:"-d,help:download item(s) under the specified path"` | |
Rename string `arg:"-r,help:rename item at the specified path"` | |
Remove bool `arg:"-x,help:delete item(s) under the specified path"` | |
Verbose bool `arg:"-v,help:verbose mode"` | |
} | |
args.Path = "/" | |
args.Recurse = 0 | |
args.User = "current_user" | |
arg.MustParse(&args) | |
p := args.Path | |
email, pwd := userCfg(args.User) | |
login(email, pwd) | |
ok, node := pathToID(p, "0") | |
if !ok { | |
os.Exit(1) | |
} | |
fmt.Printf("user: %s; node Id: %s\n\n", email, node.Id) | |
switch { | |
case args.Download: | |
if !node.IsDirectory { | |
download(node, "") | |
} else { | |
dl := downsync(node, node.Name, 0) | |
dls, _ := utils.NiceBytes(dl) | |
fmt.Printf("total downloaded: %s\n", dls) | |
} | |
case args.Info: | |
const quota = int64(2 * 1024 * 1024 * 1024) | |
fmt.Printf("computing tree size for folder %q...\n", args.Path) | |
tsize := treeSize(node.Id, 0) | |
ts, _ := utils.NiceBytes(tsize) | |
if args.Path == "/" { | |
left := quota - tsize | |
l, _ := utils.NiceBytes(left) | |
fmt.Printf("used: %10s\nleft: %10s", ts, l) | |
} else { | |
fmt.Printf("path: %s\ntotal size: %s\n", args.Path, ts) | |
} | |
case args.Tree: | |
treeList(node.Id, 0, args.Recurse) | |
case args.Mkdir != "": | |
if !node.IsDirectory { | |
fmt.Println("error:", node.Name, "is not a folder") | |
} else { | |
newf := createFolder(args.Mkdir, node.Id) | |
fmt.Printf("%+v\n", newf) | |
} | |
case args.Upload != "": | |
resp := resumableUpload(args.Upload, node) | |
fmt.Printf("%+v\n", resp) | |
case args.Rename != "": | |
resp := rename(node, args.Rename) | |
fmt.Println(resp) | |
case args.Remove: | |
resp := deleteNode(node) | |
fmt.Println(resp) | |
default: | |
lf := listFolder(node.Id) | |
sort.Sort(ByName(lf.Children)) | |
for _, c := range lf.Children { | |
if c.IsDirectory { | |
fmt.Printf("%s/\n", c.Name) | |
} else { | |
pfs, _ := strconv.Atoi(c.PhysicalFileSize) | |
fsize, _ := utils.NiceBytes(int64(pfs)) | |
fmt.Printf("%-67s %10s\n", c.Name, fsize) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Emulates browser API requests. Cookies are returned in the
login
response body