From a34ca0ff4bfa759488c72cdfc590675306bfe6cb Mon Sep 17 00:00:00 2001 From: John Date: Sat, 7 Mar 2020 19:31:33 +0800 Subject: [PATCH] improve uploading file feature for ghttp.Server; improve package gfile/gstr/gdebug --- debug/gdebug/gdebug.go | 18 ++- net/ghttp/ghttp_request_param.go | 15 +++ net/ghttp/ghttp_request_param_file.go | 45 ++++---- net/ghttp/ghttp_unit_param_file_test.go | 146 ++++++++++++++++++++++++ net/ghttp/testdata/upload/file1.txt | 1 + net/ghttp/testdata/upload/file2.txt | 1 + os/gfile/gfile.go | 43 ++++--- text/gstr/gstr.go | 10 +- 8 files changed, 235 insertions(+), 44 deletions(-) create mode 100644 net/ghttp/ghttp_unit_param_file_test.go create mode 100644 net/ghttp/testdata/upload/file1.txt create mode 100644 net/ghttp/testdata/upload/file2.txt diff --git a/debug/gdebug/gdebug.go b/debug/gdebug/gdebug.go index 6809ccfa2..e4d897a7f 100644 --- a/debug/gdebug/gdebug.go +++ b/debug/gdebug/gdebug.go @@ -10,6 +10,9 @@ package gdebug import ( "bytes" "fmt" + "io/ioutil" + "os" + "os/exec" "path/filepath" "reflect" "runtime" @@ -19,7 +22,6 @@ import ( "github.com/gogf/gf/encoding/ghash" "github.com/gogf/gf/crypto/gmd5" - "github.com/gogf/gf/os/gfile" ) const ( @@ -31,20 +33,30 @@ var ( goRootForFilter = runtime.GOROOT() // goRootForFilter is used for stack filtering purpose. binaryVersion = "" // The version of current running binary(uint64 hex). binaryVersionMd5 = "" // The version of current running binary(MD5). + selfPath = "" // Current running binary absolute path. ) func init() { if goRootForFilter != "" { goRootForFilter = strings.Replace(goRootForFilter, "\\", "/", -1) } + // Initialize internal package variable: selfPath. + selfPath, _ := exec.LookPath(os.Args[0]) + if selfPath != "" { + selfPath, _ = filepath.Abs(selfPath) + } + if selfPath == "" { + selfPath, _ = filepath.Abs(os.Args[0]) + } } // BinVersion returns the version of current running binary. // It uses ghash.BKDRHash+BASE36 algorithm to calculate the unique version of the binary. func BinVersion() string { if binaryVersion == "" { + binaryContent, _ := ioutil.ReadFile(selfPath) binaryVersion = strconv.FormatInt( - int64(ghash.BKDRHash(gfile.GetBytes(gfile.SelfPath()))), + int64(ghash.BKDRHash(binaryContent)), 36, ) } @@ -55,7 +67,7 @@ func BinVersion() string { // It uses MD5 algorithm to calculate the unique version of the binary. func BinVersionMd5() string { if binaryVersionMd5 == "" { - binaryVersionMd5, _ = gmd5.EncryptFile(gfile.SelfPath()) + binaryVersionMd5, _ = gmd5.EncryptFile(selfPath) } return binaryVersionMd5 } diff --git a/net/ghttp/ghttp_request_param.go b/net/ghttp/ghttp_request_param.go index 8b5698350..6b66a820b 100644 --- a/net/ghttp/ghttp_request_param.go +++ b/net/ghttp/ghttp_request_param.go @@ -9,6 +9,7 @@ package ghttp import ( "bytes" "encoding/json" + "fmt" "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/encoding/gjson" "github.com/gogf/gf/encoding/gurl" @@ -302,5 +303,19 @@ func (r *Request) GetMultipartFiles(name string) []*multipart.FileHeader { if v := form.File[name+"[]"]; len(v) > 0 { return v } + // Support "name[0]","name[1]","name[2]", etc. as array parameter. + key := "" + files := make([]*multipart.FileHeader, 0) + for i := 0; ; i++ { + key = fmt.Sprintf(`%s[%d]`, name, i) + if v := form.File[key]; len(v) > 0 { + files = append(files, v[0]) + } else { + break + } + } + if len(files) > 0 { + return files + } return nil } diff --git a/net/ghttp/ghttp_request_param_file.go b/net/ghttp/ghttp_request_param_file.go index e91f5b3e7..6b0de4f70 100644 --- a/net/ghttp/ghttp_request_param_file.go +++ b/net/ghttp/ghttp_request_param_file.go @@ -26,7 +26,7 @@ type UploadFile struct { // UploadFiles is array type for *UploadFile. type UploadFiles []*UploadFile -// Save saves the single uploading file to specified path. +// Save saves the single uploading file to specified path and returns the saved file name. // The parameter path can be either a directory or a file path. If is a directory, // it saves the uploading file to the directory using its original name. If is a // file path, it saves the uploading file to the file path. @@ -35,56 +35,61 @@ type UploadFiles []*UploadFile // make sense if the is a directory. // // Note that it will overwrite the target file if there's already a same name file exist. -func (f *UploadFile) Save(path string, randomlyRename ...bool) error { +func (f *UploadFile) Save(path string, randomlyRename ...bool) (filename string, err error) { if f == nil { - return nil + return } file, err := f.Open() if err != nil { - return err + return "", err } defer file.Close() filePath := path if gfile.IsDir(path) { - filename := gfile.Basename(f.Filename) + name := gfile.Basename(f.Filename) if len(randomlyRename) > 0 && randomlyRename[0] { - filename = strings.ToLower(strconv.FormatInt(gtime.TimestampNano(), 36) + grand.S(6)) - filename = filename + gfile.Ext(f.Filename) + name = strings.ToLower(strconv.FormatInt(gtime.TimestampNano(), 36) + grand.S(6)) + name = name + gfile.Ext(f.Filename) } - filePath = gfile.Join(path, filename) + filePath = gfile.Join(path, name) } newFile, err := gfile.Create(filePath) if err != nil { - return err + return "", err } defer newFile.Close() intlog.Printf(`save upload file: %s`, filePath) if _, err := io.Copy(newFile, file); err != nil { - return err + return "", err } - return nil + return gfile.Basename(filePath), nil } -// Save saves all uploading files to specified directory path. +// Save saves all uploading files to specified directory path and returns the saved file names. // // The parameter should be a directory path or it returns error. // // The parameter specifies whether randomly renames all the file names. -func (fs UploadFiles) Save(dirPath string, randomlyRename ...bool) error { +func (fs UploadFiles) Save(dirPath string, randomlyRename ...bool) (filenames []string, err error) { if len(fs) == 0 { - return nil + return nil, nil } - if !gfile.IsDir(dirPath) { - return errors.New(`parameter "dirPath" should be a directory path`) + if !gfile.Exists(dirPath) { + if err = gfile.Mkdir(dirPath); err != nil { + return + } + } else if !gfile.IsDir(dirPath) { + return nil, errors.New(`parameter "dirPath" should be a directory path`) } - var err error for _, f := range fs { - if err = f.Save(dirPath, randomlyRename...); err != nil { - return err + if filename, err := f.Save(dirPath, randomlyRename...); err != nil { + return filenames, err + } else { + filenames = append(filenames, filename) } } - return nil + return } // GetUploadFile retrieves and returns the uploading file with specified form name. diff --git a/net/ghttp/ghttp_unit_param_file_test.go b/net/ghttp/ghttp_unit_param_file_test.go new file mode 100644 index 000000000..0a5384644 --- /dev/null +++ b/net/ghttp/ghttp_unit_param_file_test.go @@ -0,0 +1,146 @@ +// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package ghttp_test + +import ( + "fmt" + "github.com/gogf/gf/debug/gdebug" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/text/gstr" + "testing" + "time" + + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/test/gtest" +) + +func Test_Params_File_Single(t *testing.T) { + dstDirPath := gfile.Join(gfile.TempDir(), gtime.TimestampNanoStr()) + err := gfile.Mkdir(dstDirPath) + gtest.Assert(err, nil) + + p := ports.PopRand() + s := g.Server(p) + s.BindHandler("/upload/single", func(r *ghttp.Request) { + file := r.GetUploadFile("file") + if file == nil { + r.Response.WriteExit("upload file cannot be empty") + } + + if name, err := file.Save(dstDirPath, r.GetBool("randomlyRename")); err == nil { + r.Response.WriteExit(name) + } + r.Response.WriteExit("upload failed") + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + time.Sleep(100 * time.Millisecond) + // normal name + gtest.Case(t, func() { + client := ghttp.NewClient() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + + srcPath := gfile.Join(gdebug.CallerDirectory(), "testdata", "upload", "file1.txt") + dstPath := gfile.Join(dstDirPath, "file1.txt") + content := client.PostContent("/upload/single", g.Map{ + "file": "@file:" + srcPath, + }) + gtest.AssertNE(content, "") + gtest.AssertNE(content, "upload file cannot be empty") + gtest.AssertNE(content, "upload failed") + gtest.Assert(content, "file1.txt") + gtest.Assert(gfile.GetContents(dstPath), gfile.GetContents(srcPath)) + }) + // randomly rename. + gtest.Case(t, func() { + client := ghttp.NewClient() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + + srcPath := gfile.Join(gdebug.CallerDirectory(), "testdata", "upload", "file2.txt") + content := client.PostContent("/upload/single", g.Map{ + "file": "@file:" + srcPath, + "randomlyRename": true, + }) + dstPath := gfile.Join(dstDirPath, content) + gtest.AssertNE(content, "") + gtest.AssertNE(content, "upload file cannot be empty") + gtest.AssertNE(content, "upload failed") + gtest.Assert(gfile.GetContents(dstPath), gfile.GetContents(srcPath)) + }) +} + +func Test_Params_File_Batch(t *testing.T) { + dstDirPath := gfile.Join(gfile.TempDir(), gtime.TimestampNanoStr()) + err := gfile.Mkdir(dstDirPath) + gtest.Assert(err, nil) + + p := ports.PopRand() + s := g.Server(p) + s.BindHandler("/upload/batch", func(r *ghttp.Request) { + files := r.GetUploadFiles("file") + if files == nil { + r.Response.WriteExit("upload file cannot be empty") + } + + if names, err := files.Save(dstDirPath, r.GetBool("randomlyRename")); err == nil { + r.Response.WriteExit(gstr.Join(names, ",")) + } + r.Response.WriteExit("upload failed") + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + time.Sleep(100 * time.Millisecond) + // normal name + gtest.Case(t, func() { + client := ghttp.NewClient() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + + srcPath1 := gfile.Join(gdebug.CallerDirectory(), "testdata", "upload", "file1.txt") + srcPath2 := gfile.Join(gdebug.CallerDirectory(), "testdata", "upload", "file2.txt") + dstPath1 := gfile.Join(dstDirPath, "file1.txt") + dstPath2 := gfile.Join(dstDirPath, "file2.txt") + content := client.PostContent("/upload/batch", g.Map{ + "file[0]": "@file:" + srcPath1, + "file[1]": "@file:" + srcPath2, + }) + gtest.AssertNE(content, "") + gtest.AssertNE(content, "upload file cannot be empty") + gtest.AssertNE(content, "upload failed") + gtest.Assert(content, "file1.txt,file2.txt") + gtest.Assert(gfile.GetContents(dstPath1), gfile.GetContents(srcPath1)) + gtest.Assert(gfile.GetContents(dstPath2), gfile.GetContents(srcPath2)) + }) + // randomly rename. + gtest.Case(t, func() { + client := ghttp.NewClient() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + + srcPath1 := gfile.Join(gdebug.CallerDirectory(), "testdata", "upload", "file1.txt") + srcPath2 := gfile.Join(gdebug.CallerDirectory(), "testdata", "upload", "file2.txt") + content := client.PostContent("/upload/batch", g.Map{ + "file[0]": "@file:" + srcPath1, + "file[1]": "@file:" + srcPath2, + "randomlyRename": true, + }) + gtest.AssertNE(content, "") + gtest.AssertNE(content, "upload file cannot be empty") + gtest.AssertNE(content, "upload failed") + + array := gstr.SplitAndTrim(content, ",") + gtest.Assert(len(array), 2) + dstPath1 := gfile.Join(dstDirPath, array[0]) + dstPath2 := gfile.Join(dstDirPath, array[1]) + gtest.Assert(gfile.GetContents(dstPath1), gfile.GetContents(srcPath1)) + gtest.Assert(gfile.GetContents(dstPath2), gfile.GetContents(srcPath2)) + }) +} diff --git a/net/ghttp/testdata/upload/file1.txt b/net/ghttp/testdata/upload/file1.txt new file mode 100644 index 000000000..1885a2771 --- /dev/null +++ b/net/ghttp/testdata/upload/file1.txt @@ -0,0 +1 @@ +file1.txt: This file is for uploading unit test case. \ No newline at end of file diff --git a/net/ghttp/testdata/upload/file2.txt b/net/ghttp/testdata/upload/file2.txt new file mode 100644 index 000000000..e3167d360 --- /dev/null +++ b/net/ghttp/testdata/upload/file2.txt @@ -0,0 +1 @@ +file2.txt: This file is for uploading unit test case. \ No newline at end of file diff --git a/os/gfile/gfile.go b/os/gfile/gfile.go index 5c101e632..bc99687dc 100644 --- a/os/gfile/gfile.go +++ b/os/gfile/gfile.go @@ -31,17 +31,32 @@ const ( var ( // Default perm for file opening. DefaultPerm = os.FileMode(0666) + // The absolute file path for main package. // It can be only checked and set once. mainPkgPath = gtype.NewString() + + // selfPath is the current running binary path. + // As it is most commonly used, it is so defined as an internal package variable. + selfPath = "" + // Temporary directory of system. tempDir = "/tmp" ) func init() { + // Initialize internal package variable: tempDir. if !Exists(tempDir) { tempDir = os.TempDir() } + // Initialize internal package variable: selfPath. + selfPath, _ := exec.LookPath(os.Args[0]) + if selfPath != "" { + selfPath, _ = filepath.Abs(selfPath) + } + if selfPath == "" { + selfPath, _ = filepath.Abs(os.Args[0]) + } } // Mkdir creates directories recursively with given . @@ -66,17 +81,20 @@ func Create(path string) (*os.File, error) { return os.Create(path) } -// Open opens file/directory readonly. +// Open opens file/directory READONLY. func Open(path string) (*os.File, error) { return os.Open(path) } -// OpenFile opens file/directory with given and . +// OpenFile opens file/directory with custom and . +// The parameter is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc. func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) { return os.OpenFile(path, flag, perm) } -// OpenWithFlag opens file/directory with default perm and given . +// OpenWithFlag opens file/directory with default perm and custom . +// The default is 0666. +// The parameter is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc. func OpenWithFlag(path string, flag int) (*os.File, error) { f, err := os.OpenFile(path, flag, DefaultPerm) if err != nil { @@ -85,9 +103,11 @@ func OpenWithFlag(path string, flag int) (*os.File, error) { return f, nil } -// OpenWithFlagPerm opens file/directory with given and . +// OpenWithFlagPerm opens file/directory with custom and . +// The parameter is like: O_RDONLY, O_RDWR, O_RDWR|O_CREATE|O_TRUNC, etc. +// The parameter is like: 0600, 0666, 0777, etc. func OpenWithFlagPerm(path string, flag int, perm os.FileMode) (*os.File, error) { - f, err := os.OpenFile(path, flag, os.FileMode(perm)) + f, err := os.OpenFile(path, flag, perm) if err != nil { return nil, err } @@ -208,6 +228,7 @@ func Glob(pattern string, onlyNames ...bool) ([]string, error) { // Remove deletes all file/directory with parameter. // If parameter is directory, it deletes it recursively. func Remove(path string) error { + //intlog.Print(`Remove:`, path) return os.RemoveAll(path) } @@ -278,17 +299,7 @@ func RealPath(path string) string { // SelfPath returns absolute file path of current running process(binary). func SelfPath() string { - path, _ := exec.LookPath(os.Args[0]) - if path != "" { - path, _ = filepath.Abs(path) - if path != "" { - return path - } - } - if path == "" { - path, _ = filepath.Abs(os.Args[0]) - } - return path + return selfPath } // SelfName returns file name of current running process(binary). diff --git a/text/gstr/gstr.go b/text/gstr/gstr.go index 1f504c15c..27b61f6d5 100644 --- a/text/gstr/gstr.go +++ b/text/gstr/gstr.go @@ -465,16 +465,16 @@ func SplitAndTrimSpace(str, delimiter string) []string { return array } -// Join concatenates the elements of a to create a single string. The separator string -// sep is placed between elements in the resulting string. +// Join concatenates the elements of to create a single string. The separator string +// is placed between elements in the resulting string. func Join(array []string, sep string) string { return strings.Join(array, sep) } -// JoinAny concatenates the elements of a to create a single string. The separator string -// sep is placed between elements in the resulting string. +// JoinAny concatenates the elements of to create a single string. The separator string +// is placed between elements in the resulting string. // -// The parameter can be any type of slice. +// The parameter can be any type of slice, which be converted to string array. func JoinAny(array interface{}, sep string) string { return strings.Join(gconv.Strings(array), sep) }