!1 Merge back to master

Merge pull request !1 from dev branch
This commit is contained in:
hekk 2018-05-27 21:32:55 +08:00
parent 30c963a5f0
commit 61a1c93f0f
89 changed files with 15859 additions and 2 deletions

105
server/apis/auth.go Normal file
View file

@ -0,0 +1,105 @@
package apis
import (
"net/http"
"time"
)
import (
"quickshare/server/libs/httputil"
"quickshare/server/libs/httpworker"
)
func (srv *SrvShare) LoginHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
srv.Http.Fill(httputil.Err404, res)
return
}
act := req.FormValue(srv.Conf.KeyAct)
todo := func(res http.ResponseWriter, req *http.Request) interface{} { return httputil.Err404 }
switch act {
case srv.Conf.ActLogin:
todo = srv.Login
case srv.Conf.ActLogout:
todo = srv.Logout
default:
srv.Http.Fill(httputil.Err404, res)
return
}
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&httpworker.Task{
Ack: ack,
Do: srv.Wrap(todo),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httputil.Err503, res)
return
}
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httputil.Err500, res)
}
}
func (srv *SrvShare) Login(res http.ResponseWriter, req *http.Request) interface{} {
// all users need to pass same wall to login
if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) ||
!srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdLogin) {
return httputil.Err504
}
return srv.login(
req.FormValue(srv.Conf.KeyAdminId),
req.FormValue(srv.Conf.KeyAdminPwd),
res,
)
}
func (srv *SrvShare) login(adminId string, adminPwd string, res http.ResponseWriter) interface{} {
if adminId != srv.Conf.AdminId ||
adminPwd != srv.Conf.AdminPwd {
return httputil.Err401
}
token := srv.Walls.MakeLoginToken(srv.Conf.AdminId)
if token == "" {
return httputil.Err500
}
srv.Http.SetCookie(res, srv.Conf.KeyToken, token)
return httputil.Ok200
}
func (srv *SrvShare) Logout(res http.ResponseWriter, req *http.Request) interface{} {
srv.Http.SetCookie(res, srv.Conf.KeyToken, "-")
return httputil.Ok200
}
func (srv *SrvShare) IsValidLength(length int64) bool {
return length > 0 && length <= srv.Conf.MaxUpBytesPerSec
}
func (srv *SrvShare) IsValidStart(start, expectStart int64) bool {
return start == expectStart
}
func (srv *SrvShare) IsValidShareId(shareId string) bool {
// id could be 0 for dev environment
if srv.Conf.Production {
return len(shareId) == 64
}
return true
}
func (srv *SrvShare) IsValidDownLimit(limit int) bool {
return limit >= -1
}
func IsValidFileName(fileName string) bool {
return fileName != "" && len(fileName) < 240
}

78
server/apis/auth_test.go Normal file
View file

@ -0,0 +1,78 @@
package apis
import (
"fmt"
"strings"
"testing"
)
import (
"quickshare/server/libs/cfg"
"quickshare/server/libs/encrypt"
"quickshare/server/libs/httputil"
)
func TestLogin(t *testing.T) {
conf := cfg.NewConfig()
type testCase struct {
Desc string
AdminId string
AdminPwd string
Result interface{}
VerifyToken bool
}
testCases := []testCase{
testCase{
Desc: "invalid input",
AdminId: "",
AdminPwd: "",
Result: httputil.Err401,
VerifyToken: false,
},
testCase{
Desc: "account not match",
AdminId: "unknown",
AdminPwd: "unknown",
Result: httputil.Err401,
VerifyToken: false,
},
testCase{
Desc: "succeed to login",
AdminId: conf.AdminId,
AdminPwd: conf.AdminPwd,
Result: httputil.Ok200,
VerifyToken: true,
},
}
for _, testCase := range testCases {
srv := NewSrvShare(conf)
res := &stubWriter{Headers: map[string][]string{}}
ret := srv.login(testCase.AdminId, testCase.AdminPwd, res)
if ret != testCase.Result {
t.Fatalf("login: reponse=%v testCase=%v", ret, testCase.Result)
}
// verify cookie (only token.adminid part))
if testCase.VerifyToken {
cookieVal := strings.Replace(
res.Header().Get("Set-Cookie"),
fmt.Sprintf("%s=", conf.KeyToken),
"",
1,
)
gotTokenStr := strings.Split(cookieVal, ";")[0]
token := encrypt.JwtEncrypterMaker(conf.SecretKey)
token.FromStr(gotTokenStr)
gotToken, found := token.Get(conf.KeyAdminId)
if !found || conf.AdminId != gotToken {
t.Fatalf("login: token admin id unmatch got=%v expect=%v", gotToken, conf.AdminId)
}
}
}
}

66
server/apis/client.go Normal file
View file

@ -0,0 +1,66 @@
package apis
import (
"net/http"
"path/filepath"
"strings"
"time"
)
import (
"quickshare/server/libs/httputil"
"quickshare/server/libs/httpworker"
)
func (srv *SrvShare) ClientHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
srv.Http.Fill(httputil.Err404, res)
return
}
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&httpworker.Task{
Ack: ack,
Do: srv.Wrap(srv.GetClient),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httputil.Err503, res)
return
}
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httputil.Err500, res)
}
}
func (srv *SrvShare) GetClient(res http.ResponseWriter, req *http.Request) interface{} {
if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) {
return httputil.Err504
}
return srv.getClient(res, req, req.URL.EscapedPath())
}
func (srv *SrvShare) getClient(res http.ResponseWriter, req *http.Request, relPath string) interface{} {
if strings.HasSuffix(relPath, "/") {
relPath = relPath + "index.html"
}
if !IsValidClientPath(relPath) {
return httputil.Err400
}
fullPath := filepath.Clean(filepath.Join("./public", relPath))
http.ServeFile(res, req, fullPath)
return 0
}
func IsValidClientPath(fullPath string) bool {
if strings.Contains(fullPath, "..") {
return false
}
return true
}

69
server/apis/download.go Normal file
View file

@ -0,0 +1,69 @@
package apis
import (
"net/http"
"time"
)
import (
"quickshare/server/libs/fileidx"
"quickshare/server/libs/httputil"
"quickshare/server/libs/httpworker"
)
func (srv *SrvShare) DownloadHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
srv.Http.Fill(httputil.Err404, res)
}
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&httpworker.Task{
Ack: ack,
Do: srv.Wrap(srv.Download),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httputil.Err503, res)
}
// using WriteTimeout instead of Timeout
// After timeout, connection will be lost, and worker will fail to write and return
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.WriteTimeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httputil.Err500, res)
}
}
func (srv *SrvShare) Download(res http.ResponseWriter, req *http.Request) interface{} {
shareId := req.FormValue(srv.Conf.KeyShareId)
if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) ||
!srv.Walls.PassOpLimit(shareId, srv.Conf.OpIdDownload) {
return httputil.Err429
}
return srv.download(shareId, res, req)
}
func (srv *SrvShare) download(shareId string, res http.ResponseWriter, req *http.Request) interface{} {
if !srv.IsValidShareId(shareId) {
return httputil.Err400
}
fileInfo, found := srv.Index.Get(shareId)
switch {
case !found || fileInfo.State != fileidx.StateDone:
return httputil.Err404
case fileInfo.DownLimit == 0:
return httputil.Err412
default:
updated, _ := srv.Index.DecrDownLimit(shareId)
if updated != 1 {
return httputil.Err500
}
}
err := srv.Downloader.ServeFile(res, req, fileInfo)
srv.Err.IsErr(err)
return 0
}

View file

@ -0,0 +1,271 @@
package apis
import (
"net/http"
"os"
"testing"
"time"
)
import (
"quickshare/server/libs/cfg"
"quickshare/server/libs/errutil"
"quickshare/server/libs/fileidx"
"quickshare/server/libs/httputil"
"quickshare/server/libs/logutil"
"quickshare/server/libs/qtube"
)
func initServiceForDownloadTest(config *cfg.Config, indexMap map[string]*fileidx.FileInfo, content string) *SrvShare {
setDownloader := func(srv *SrvShare) {
srv.Downloader = stubDownloader{Content: content}
}
setIndex := func(srv *SrvShare) {
srv.Index = fileidx.NewMemFileIndexWithMap(len(indexMap), indexMap)
}
setFs := func(srv *SrvShare) {
srv.Fs = &stubFsUtil{
MockFile: &qtube.StubFile{
Content: content,
Offset: 0,
},
}
}
logger := logutil.NewSlog(os.Stdout, config.AppName)
setLog := func(srv *SrvShare) {
srv.Log = logger
}
setErr := func(srv *SrvShare) {
srv.Err = errutil.NewErrChecker(!config.Production, logger)
}
return InitSrvShare(config, setDownloader, setIndex, setFs, setLog, setErr)
}
func TestDownload(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
Content string
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response interface{}
Body string
}
type testCase struct {
Desc string
Init
Input
Output
}
testCases := []testCase{
testCase{
Desc: "empty file index",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err404,
},
},
testCase{
Desc: "file info not found",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"1": &fileidx.FileInfo{},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"1": &fileidx.FileInfo{},
},
Response: httputil.Err404,
},
},
testCase{
Desc: "file not found because of state=uploading",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: 1,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateUploading,
Uploaded: 1,
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: 1,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateUploading,
Uploaded: 1,
},
},
Response: httputil.Err404,
},
},
testCase{
Desc: "download failed because download limit = 0",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: 0,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateDone,
Uploaded: 1,
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: 0,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateDone,
Uploaded: 1,
},
},
Response: httputil.Err412,
},
},
testCase{
Desc: "succeed to download",
Init: Init{
Content: "content",
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: 1,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateDone,
Uploaded: 1,
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: 0,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateDone,
Uploaded: 1,
},
},
Response: 0,
Body: "content",
},
},
testCase{
Desc: "succeed to download DownLimit == -1",
Init: Init{
Content: "content",
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: -1,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateDone,
Uploaded: 1,
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: -1,
ModTime: time.Now().UnixNano(),
PathLocal: "path",
State: fileidx.StateDone,
Uploaded: 1,
},
},
Response: 0,
Body: "content",
},
},
}
for _, testCase := range testCases {
srv := initServiceForDownloadTest(conf, testCase.Init.IndexMap, testCase.Content)
writer := &stubWriter{Headers: map[string][]string{}}
response := srv.download(
testCase.ShareId,
writer,
&http.Request{},
)
// verify downlimit
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
info, _ := srv.Index.Get(testCase.ShareId)
t.Fatalf(
"download: index incorrect got=%v want=%v",
info,
testCase.Output.IndexMap[testCase.ShareId],
)
}
// verify response
if response != testCase.Output.Response {
t.Fatalf(
"download: response incorrect response=%v testCase=%v",
response,
testCase.Output.Response,
)
}
// verify writerContent
if string(writer.Response) != testCase.Output.Body {
t.Fatalf(
"download: body incorrect got=%v want=%v",
string(writer.Response),
testCase.Output.Body,
)
}
}
}

234
server/apis/file_info.go Normal file
View file

@ -0,0 +1,234 @@
package apis
import (
"fmt"
"math/rand"
"net/http"
"path/filepath"
"strconv"
"time"
)
import (
"quickshare/server/libs/fileidx"
"quickshare/server/libs/httputil"
"quickshare/server/libs/httpworker"
)
func (srv *SrvShare) FileInfoHandler(res http.ResponseWriter, req *http.Request) {
tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
if !srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr)) ||
!srv.Walls.PassLoginCheck(tokenStr, req) {
srv.Http.Fill(httputil.Err429, res)
return
}
todo := func(res http.ResponseWriter, req *http.Request) interface{} { return httputil.Err404 }
switch req.Method {
case http.MethodGet:
todo = srv.List
case http.MethodDelete:
todo = srv.Del
case http.MethodPatch:
act := req.FormValue(srv.Conf.KeyAct)
switch act {
case srv.Conf.ActShadowId:
todo = srv.ShadowId
case srv.Conf.ActPublishId:
todo = srv.PublishId
case srv.Conf.ActSetDownLimit:
todo = srv.SetDownLimit
case srv.Conf.ActAddLocalFiles:
todo = srv.AddLocalFiles
default:
srv.Http.Fill(httputil.Err404, res)
return
}
default:
srv.Http.Fill(httputil.Err404, res)
return
}
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&httpworker.Task{
Ack: ack,
Do: srv.Wrap(todo),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httputil.Err503, res)
}
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httputil.Err500, res)
}
}
type ResInfos struct {
List []*fileidx.FileInfo
}
func (srv *SrvShare) List(res http.ResponseWriter, req *http.Request) interface{} {
if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdGetFInfo) {
return httputil.Err429
}
return srv.list()
}
func (srv *SrvShare) list() interface{} {
infos := make([]*fileidx.FileInfo, 0)
for _, info := range srv.Index.List() {
infos = append(infos, info)
}
return &ResInfos{List: infos}
}
func (srv *SrvShare) Del(res http.ResponseWriter, req *http.Request) interface{} {
shareId := req.FormValue(srv.Conf.KeyShareId)
if !srv.Walls.PassOpLimit(shareId, srv.Conf.OpIdDelFInfo) {
return httputil.Err504
}
return srv.del(shareId)
}
func (srv *SrvShare) del(shareId string) interface{} {
if !srv.IsValidShareId(shareId) {
return httputil.Err400
}
fileInfo, found := srv.Index.Get(shareId)
if !found {
return httputil.Err404
}
srv.Index.Del(shareId)
fullPath := filepath.Join(srv.Conf.PathLocal, fileInfo.PathLocal)
if !srv.Fs.DelFile(fullPath) {
// TODO: may log file name because file not exist or delete is not authenticated
return httputil.Err500
}
return httputil.Ok200
}
func (srv *SrvShare) ShadowId(res http.ResponseWriter, req *http.Request) interface{} {
if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdOpFInfo) {
return httputil.Err429
}
shareId := req.FormValue(srv.Conf.KeyShareId)
return srv.shadowId(shareId)
}
func (srv *SrvShare) shadowId(shareId string) interface{} {
if !srv.IsValidShareId(shareId) {
return httputil.Err400
}
info, found := srv.Index.Get(shareId)
if !found {
return httputil.Err404
}
secretId := srv.Encryptor.Encrypt(
[]byte(fmt.Sprintf("%s%s", info.PathLocal, genPwd())),
)
if !srv.Index.SetId(info.Id, secretId) {
return httputil.Err412
}
return &ShareInfo{ShareId: secretId}
}
func (srv *SrvShare) PublishId(res http.ResponseWriter, req *http.Request) interface{} {
if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdOpFInfo) {
return httputil.Err429
}
shareId := req.FormValue(srv.Conf.KeyShareId)
return srv.publishId(shareId)
}
func (srv *SrvShare) publishId(shareId string) interface{} {
if !srv.IsValidShareId(shareId) {
return httputil.Err400
}
info, found := srv.Index.Get(shareId)
if !found {
return httputil.Err404
}
publicId := srv.Encryptor.Encrypt([]byte(info.PathLocal))
if !srv.Index.SetId(info.Id, publicId) {
return httputil.Err412
}
return &ShareInfo{ShareId: publicId}
}
func (srv *SrvShare) SetDownLimit(res http.ResponseWriter, req *http.Request) interface{} {
if !srv.Walls.PassOpLimit(srv.Conf.AllUsers, srv.Conf.OpIdOpFInfo) {
return httputil.Err429
}
shareId := req.FormValue(srv.Conf.KeyShareId)
downLimit64, downLimitParseErr := strconv.ParseInt(req.FormValue(srv.Conf.KeyDownLimit), 10, 32)
downLimit := int(downLimit64)
if srv.Err.IsErr(downLimitParseErr) {
return httputil.Err400
}
return srv.setDownLimit(shareId, downLimit)
}
func (srv *SrvShare) setDownLimit(shareId string, downLimit int) interface{} {
if !srv.IsValidShareId(shareId) || !srv.IsValidDownLimit(downLimit) {
return httputil.Err400
}
if !srv.Index.SetDownLimit(shareId, downLimit) {
return httputil.Err404
}
return httputil.Ok200
}
func (srv *SrvShare) AddLocalFiles(res http.ResponseWriter, req *http.Request) interface{} {
return srv.AddLocalFilesImp()
}
func (srv *SrvShare) AddLocalFilesImp() interface{} {
infos, err := srv.Fs.Readdir(srv.Conf.PathLocal, srv.Conf.LocalFileLimit)
if srv.Err.IsErr(err) {
panic(fmt.Sprintf("fail to readdir: %v", err))
}
for _, info := range infos {
info.DownLimit = srv.Conf.DownLimit
info.State = fileidx.StateDone
info.PathLocal = info.PathLocal
info.Id = srv.Encryptor.Encrypt([]byte(info.PathLocal))
addRet := srv.Index.Add(info)
switch {
case addRet == 0 || addRet == -1:
// TODO: return files not added
continue
case addRet == 1:
break
default:
return httputil.Err500
}
}
return httputil.Ok200
}
func genPwd() string {
return fmt.Sprintf("%d%d%d%d", rand.Intn(10), rand.Intn(10), rand.Intn(10), rand.Intn(10))
}

View file

@ -0,0 +1,584 @@
package apis
import (
"os"
"path/filepath"
"testing"
)
import (
"quickshare/server/libs/cfg"
"quickshare/server/libs/errutil"
"quickshare/server/libs/fileidx"
"quickshare/server/libs/httputil"
"quickshare/server/libs/logutil"
)
const mockShadowId = "shadowId"
const mockPublicId = "publicId"
func initServiceForFileInfoTest(
config *cfg.Config,
indexMap map[string]*fileidx.FileInfo,
useShadowEnc bool,
localFileInfos []*fileidx.FileInfo,
) *SrvShare {
setIndex := func(srv *SrvShare) {
srv.Index = fileidx.NewMemFileIndexWithMap(len(indexMap), indexMap)
}
setFs := func(srv *SrvShare) {
srv.Fs = &stubFsUtil{MockLocalFileInfos: localFileInfos}
}
logger := logutil.NewSlog(os.Stdout, config.AppName)
setLog := func(srv *SrvShare) {
srv.Log = logger
}
errChecker := errutil.NewErrChecker(!config.Production, logger)
setErr := func(srv *SrvShare) {
srv.Err = errChecker
}
var setEncryptor AddDep
if useShadowEnc {
setEncryptor = func(srv *SrvShare) {
srv.Encryptor = &stubEncryptor{MockResult: mockShadowId}
}
} else {
setEncryptor = func(srv *SrvShare) {
srv.Encryptor = &stubEncryptor{MockResult: mockPublicId}
}
}
return InitSrvShare(config, setIndex, setFs, setEncryptor, setLog, setErr)
}
func TestList(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Output struct {
IndexMap map[string]*fileidx.FileInfo
}
type TestCase struct {
Desc string
Output
}
testCases := []TestCase{
TestCase{
Desc: "success",
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
},
"1": &fileidx.FileInfo{
Id: "1",
},
},
},
},
}
for _, testCase := range testCases {
srv := initServiceForFileInfoTest(conf, testCase.Output.IndexMap, true, []*fileidx.FileInfo{})
response := srv.list()
resInfos := response.(*ResInfos)
for _, info := range resInfos.List {
infoFromSrv, found := srv.Index.Get(info.Id)
if !found || infoFromSrv.Id != info.Id {
t.Fatalf("list: file infos are not identical")
}
}
if len(resInfos.List) != len(srv.Index.List()) {
t.Fatalf("list: file infos are not identical")
}
}
}
func TestDel(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response httputil.MsgRes
}
type TestCase struct {
Desc string
Init
Input
Output
}
testCases := []TestCase{
TestCase{
Desc: "success",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
},
"1": &fileidx.FileInfo{
Id: "1",
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"1": &fileidx.FileInfo{
Id: "1",
},
},
Response: httputil.Ok200,
},
},
TestCase{
Desc: "not found",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"1": &fileidx.FileInfo{
Id: "1",
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"1": &fileidx.FileInfo{
Id: "1",
},
},
Response: httputil.Err404,
},
},
}
for _, testCase := range testCases {
srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, true, []*fileidx.FileInfo{})
response := srv.del(testCase.ShareId)
res := response.(httputil.MsgRes)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf("del: index incorrect")
}
if res != testCase.Output.Response {
t.Fatalf("del: response incorrect got: %v, want: %v", res, testCase.Output.Response)
}
}
}
func TestShadowId(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response interface{}
}
type TestCase struct {
Desc string
Init
Input
Output
}
testCases := []TestCase{
TestCase{
Desc: "success",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
mockShadowId: &fileidx.FileInfo{
Id: mockShadowId,
},
},
Response: &ShareInfo{
ShareId: mockShadowId,
},
},
},
TestCase{
Desc: "original id not exists",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err404,
},
},
TestCase{
Desc: "dest id exists",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
},
mockShadowId: &fileidx.FileInfo{
Id: mockShadowId,
},
},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
},
mockShadowId: &fileidx.FileInfo{
Id: mockShadowId,
},
},
Response: httputil.Err412,
},
},
}
for _, testCase := range testCases {
srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, true, []*fileidx.FileInfo{})
response := srv.shadowId(testCase.ShareId)
switch response.(type) {
case *ShareInfo:
res := response.(*ShareInfo)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
info, found := srv.Index.Get(mockShadowId)
t.Fatalf(
"shadowId: index incorrect got %v found: %v want %v",
info,
found,
testCase.Output.IndexMap[mockShadowId],
)
}
if res.ShareId != mockShadowId {
t.Fatalf("shadowId: mockId incorrect")
}
case httputil.MsgRes:
res := response.(httputil.MsgRes)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf("shadowId: map not identical")
}
if res != testCase.Output.Response {
t.Fatalf("shadowId: response incorrect")
}
default:
t.Fatalf("shadowId: return type not found")
}
}
}
func TestPublishId(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response interface{}
}
type TestCase struct {
Desc string
Init
Input
Output
}
testCases := []TestCase{
TestCase{
Desc: "success",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
mockShadowId: &fileidx.FileInfo{
Id: mockShadowId,
},
},
},
Input: Input{
ShareId: mockShadowId,
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
mockPublicId: &fileidx.FileInfo{
Id: mockPublicId,
},
},
Response: &ShareInfo{
ShareId: mockPublicId,
},
},
},
TestCase{
Desc: "original id not exists",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err404,
},
},
TestCase{
Desc: "dest id exists",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
mockShadowId: &fileidx.FileInfo{
Id: mockShadowId,
},
mockPublicId: &fileidx.FileInfo{
Id: mockPublicId,
},
},
},
Input: Input{
ShareId: mockShadowId,
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
mockShadowId: &fileidx.FileInfo{
Id: mockShadowId,
},
mockPublicId: &fileidx.FileInfo{
Id: mockPublicId,
},
},
Response: httputil.Err412,
},
},
}
for _, testCase := range testCases {
srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, false, []*fileidx.FileInfo{})
response := srv.publishId(testCase.ShareId)
switch response.(type) {
case *ShareInfo:
res := response.(*ShareInfo)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
info, found := srv.Index.Get(mockPublicId)
t.Fatalf(
"shadowId: index incorrect got %v found: %v want %v",
info,
found,
testCase.Output.IndexMap[mockPublicId],
)
}
if res.ShareId != mockPublicId {
t.Fatalf("shadowId: mockId incorrect", res.ShareId, mockPublicId)
}
case httputil.MsgRes:
res := response.(httputil.MsgRes)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf("shadowId: map not identical")
}
if res != testCase.Output.Response {
t.Fatalf("shadowId: response incorrect got: %v want: %v", res, testCase.Output.Response)
}
default:
t.Fatalf("shadowId: return type not found")
}
}
}
func TestSetDownLimit(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
mockDownLimit := 100
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
DownLimit int
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response httputil.MsgRes
}
type TestCase struct {
Desc string
Init
Input
Output
}
testCases := []TestCase{
TestCase{
Desc: "success",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
},
},
},
Input: Input{
ShareId: "0",
DownLimit: mockDownLimit,
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
"0": &fileidx.FileInfo{
Id: "0",
DownLimit: mockDownLimit,
},
},
Response: httputil.Ok200,
},
},
TestCase{
Desc: "not found",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
ShareId: "0",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err404,
},
},
}
for _, testCase := range testCases {
srv := initServiceForFileInfoTest(conf, testCase.Init.IndexMap, true, []*fileidx.FileInfo{})
response := srv.setDownLimit(testCase.ShareId, mockDownLimit)
res := response.(httputil.MsgRes)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
info, _ := srv.Index.Get(testCase.ShareId)
t.Fatalf(
"setDownLimit: index incorrect got: %v want: %v",
info,
testCase.Output.IndexMap[testCase.ShareId],
)
}
if res != testCase.Output.Response {
t.Fatalf("setDownLimit: response incorrect got: %v, want: %v", res, testCase.Output.Response)
}
}
}
func TestAddLocalFiles(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
Infos []*fileidx.FileInfo
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response httputil.MsgRes
}
type TestCase struct {
Desc string
Init
Output
}
testCases := []TestCase{
TestCase{
Desc: "success",
Init: Init{
Infos: []*fileidx.FileInfo{
&fileidx.FileInfo{
Id: "",
DownLimit: 0,
ModTime: 13,
PathLocal: "filename1",
State: "",
Uploaded: 13,
},
},
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
mockPublicId: &fileidx.FileInfo{
Id: mockPublicId,
DownLimit: conf.DownLimit,
ModTime: 13,
PathLocal: filepath.Join(conf.PathLocal, "filename1"),
State: fileidx.StateDone,
Uploaded: 13,
},
},
},
},
}
for _, testCase := range testCases {
srv := initServiceForFileInfoTest(conf, testCase.Output.IndexMap, false, testCase.Init.Infos)
response := srv.AddLocalFilesImp()
res := response.(httputil.MsgRes)
if res.Code != 200 {
t.Fatalf("addLocalFiles: code not correct")
}
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf(
"addLocalFiles: indexes not identical got: %v want: %v",
srv.Index.List(),
testCase.Output.IndexMap,
)
}
}
}

145
server/apis/service.go Normal file
View file

@ -0,0 +1,145 @@
package apis
import (
"log"
"net/http"
"os"
"strings"
)
import (
"quickshare/server/libs/cfg"
"quickshare/server/libs/encrypt"
"quickshare/server/libs/errutil"
"quickshare/server/libs/fileidx"
"quickshare/server/libs/fsutil"
"quickshare/server/libs/httputil"
"quickshare/server/libs/httpworker"
"quickshare/server/libs/limiter"
"quickshare/server/libs/logutil"
"quickshare/server/libs/qtube"
"quickshare/server/libs/walls"
)
type AddDep func(*SrvShare)
func NewSrvShare(config *cfg.Config) *SrvShare {
logger := logutil.NewSlog(os.Stdout, config.AppName)
setLog := func(srv *SrvShare) {
srv.Log = logger
}
errChecker := errutil.NewErrChecker(!config.Production, logger)
setErr := func(srv *SrvShare) {
srv.Err = errChecker
}
setWorkerPool := func(srv *SrvShare) {
workerPoolSize := config.WorkerPoolSize
taskQueueSize := config.TaskQueueSize
srv.WorkerPool = httpworker.NewWorkerPool(workerPoolSize, taskQueueSize, logger)
}
setWalls := func(srv *SrvShare) {
encrypterMaker := encrypt.JwtEncrypterMaker
ipLimiter := limiter.NewRateLimiter(
config.LimiterCap,
config.LimiterTtl,
config.LimiterCyc,
config.BucketCap,
config.SpecialCaps,
)
opLimiter := limiter.NewRateLimiter(
config.LimiterCap,
config.LimiterTtl,
config.LimiterCyc,
config.BucketCap,
config.SpecialCaps,
)
srv.Walls = walls.NewAccessWalls(config, ipLimiter, opLimiter, encrypterMaker)
}
setIndex := func(srv *SrvShare) {
srv.Index = fileidx.NewMemFileIndex(config.MaxShares)
}
fs := fsutil.NewSimpleFs(errChecker)
setFs := func(srv *SrvShare) {
srv.Fs = fs
}
setDownloader := func(srv *SrvShare) {
srv.Downloader = qtube.NewQTube(
config.PathLocal,
config.MaxDownBytesPerSec,
config.MaxRangeLength,
fs,
)
}
setEncryptor := func(srv *SrvShare) {
srv.Encryptor = &encrypt.HmacEncryptor{Key: config.SecretKeyByte}
}
setHttp := func(srv *SrvShare) {
srv.Http = &httputil.QHttpUtil{
CookieDomain: config.CookieDomain,
CookieHttpOnly: config.CookieHttpOnly,
CookieMaxAge: config.CookieMaxAge,
CookiePath: config.CookiePath,
CookieSecure: config.CookieSecure,
Err: errChecker,
}
}
return InitSrvShare(config, setIndex, setWalls, setWorkerPool, setFs, setDownloader, setEncryptor, setLog, setErr, setHttp)
}
func InitSrvShare(config *cfg.Config, addDeps ...AddDep) *SrvShare {
srv := &SrvShare{}
srv.Conf = config
for _, addDep := range addDeps {
addDep(srv)
}
if !srv.Fs.MkdirAll(srv.Conf.PathLocal, os.FileMode(0775)) {
panic("fail to make ./files/ folder")
}
if res := srv.AddLocalFilesImp(); res != httputil.Ok200 {
panic("fail to add local files")
}
return srv
}
type SrvShare struct {
Conf *cfg.Config
Encryptor encrypt.Encryptor
Err errutil.ErrUtil
Downloader qtube.Downloader
Http httputil.HttpUtil
Index fileidx.FileIndex
Fs fsutil.FsUtil
Log logutil.LogUtil
Walls walls.Walls
WorkerPool httpworker.Workers
}
func (srv *SrvShare) Wrap(serviceFunc httpworker.ServiceFunc) httpworker.DoFunc {
return func(res http.ResponseWriter, req *http.Request) {
body := serviceFunc(res, req)
if body != nil && body != 0 && srv.Http.Fill(body, res) <= 0 {
log.Println("Wrap: fail to fill body", body, res)
}
}
}
func GetRemoteIp(addr string) string {
addrParts := strings.Split(addr, ":")
if len(addrParts) > 0 {
return addrParts[0]
}
return "unknown ip"
}

117
server/apis/test_helper.go Normal file
View file

@ -0,0 +1,117 @@
package apis
import (
"fmt"
"io"
"net/http"
"os"
)
import (
"quickshare/server/libs/fileidx"
"quickshare/server/libs/qtube"
)
type stubFsUtil struct {
MockLocalFileInfos []*fileidx.FileInfo
MockFile *qtube.StubFile
}
var expectCreateFileName = ""
func (fs *stubFsUtil) CreateFile(fileName string) error {
if fileName != expectCreateFileName {
panic(
fmt.Sprintf("CreateFile: got: %s expect: %s", fileName, expectCreateFileName),
)
}
return nil
}
func (fs *stubFsUtil) CopyChunkN(fullPath string, chunk io.Reader, start int64, len int64) bool {
return true
}
func (fs *stubFsUtil) ServeFile(res http.ResponseWriter, req *http.Request, fileName string) {
return
}
func (fs *stubFsUtil) DelFile(fullPath string) bool {
return true
}
func (fs *stubFsUtil) MkdirAll(path string, mode os.FileMode) bool {
return true
}
func (fs *stubFsUtil) Readdir(dirname string, n int) ([]*fileidx.FileInfo, error) {
return fs.MockLocalFileInfos, nil
}
func (fs *stubFsUtil) Open(filePath string) (qtube.ReadSeekCloser, error) {
return fs.MockFile, nil
}
type stubWriter struct {
Headers http.Header
Response []byte
StatusCode int
}
func (w *stubWriter) Header() http.Header {
return w.Headers
}
func (w *stubWriter) Write(body []byte) (int, error) {
w.Response = append(w.Response, body...)
return len(body), nil
}
func (w *stubWriter) WriteHeader(statusCode int) {
w.StatusCode = statusCode
}
type stubDownloader struct {
Content string
}
func (d stubDownloader) ServeFile(w http.ResponseWriter, r *http.Request, fileInfo *fileidx.FileInfo) error {
_, err := w.Write([]byte(d.Content))
return err
}
func sameInfoWithoutTime(info1, info2 *fileidx.FileInfo) bool {
return info1.Id == info2.Id &&
info1.DownLimit == info2.DownLimit &&
info1.PathLocal == info2.PathLocal &&
info1.State == info2.State &&
info1.Uploaded == info2.Uploaded
}
func sameMap(map1, map2 map[string]*fileidx.FileInfo) bool {
for key, info1 := range map1 {
info2, found := map2[key]
if !found || !sameInfoWithoutTime(info1, info2) {
fmt.Printf("infos are not same: \n%v \n%v", info1, info2)
return false
}
}
for key, info2 := range map2 {
info1, found := map1[key]
if !found || !sameInfoWithoutTime(info1, info2) {
fmt.Printf("infos are not same: \n%v \n%v", info1, info2)
return false
}
}
return true
}
type stubEncryptor struct {
MockResult string
}
func (enc *stubEncryptor) Encrypt(content []byte) string {
return enc.MockResult
}

253
server/apis/upload.go Normal file
View file

@ -0,0 +1,253 @@
package apis
import (
"io"
"net/http"
"path/filepath"
"strconv"
"time"
)
import (
"quickshare/server/libs/encrypt"
"quickshare/server/libs/fileidx"
"quickshare/server/libs/fsutil"
httpUtil "quickshare/server/libs/httputil"
worker "quickshare/server/libs/httpworker"
)
const DefaultId = "0"
type ByteRange struct {
ShareId string
Start int64
Length int64
}
type ShareInfo struct {
ShareId string
}
func (srv *SrvShare) StartUploadHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
srv.Http.Fill(httpUtil.Err404, res)
return
}
tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
ipPass := srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr))
loginPass := srv.Walls.PassLoginCheck(tokenStr, req)
opPass := srv.Walls.PassOpLimit(GetRemoteIp(req.RemoteAddr), srv.Conf.OpIdUpload)
if !ipPass || !loginPass || !opPass {
srv.Http.Fill(httpUtil.Err429, res)
return
}
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&worker.Task{
Ack: ack,
Do: srv.Wrap(srv.StartUpload),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httpUtil.Err503, res)
}
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httpUtil.Err500, res)
}
}
func (srv *SrvShare) UploadHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
srv.Http.Fill(httpUtil.Err404, res)
return
}
tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
ipPass := srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr))
loginPass := srv.Walls.PassLoginCheck(tokenStr, req)
opPass := srv.Walls.PassOpLimit(GetRemoteIp(req.RemoteAddr), srv.Conf.OpIdUpload)
if !ipPass || !loginPass || !opPass {
srv.Http.Fill(httpUtil.Err429, res)
return
}
multiFormErr := req.ParseMultipartForm(srv.Conf.ParseFormBufSize)
if srv.Err.IsErr(multiFormErr) {
srv.Http.Fill(httpUtil.Err400, res)
return
}
srv.Log.Println("form", req.Form)
srv.Log.Println("pform", req.PostForm)
srv.Log.Println("mform", req.MultipartForm)
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&worker.Task{
Ack: ack,
Do: srv.Wrap(srv.Upload),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httpUtil.Err503, res)
}
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httpUtil.Err500, res)
}
}
func (srv *SrvShare) FinishUploadHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
srv.Http.Fill(httpUtil.Err404, res)
return
}
tokenStr := srv.Http.GetCookie(req.Cookies(), srv.Conf.KeyToken)
ipPass := srv.Walls.PassIpLimit(GetRemoteIp(req.RemoteAddr))
loginPass := srv.Walls.PassLoginCheck(tokenStr, req)
opPass := srv.Walls.PassOpLimit(GetRemoteIp(req.RemoteAddr), srv.Conf.OpIdUpload)
if !ipPass || !loginPass || !opPass {
srv.Http.Fill(httpUtil.Err429, res)
return
}
ack := make(chan error, 1)
ok := srv.WorkerPool.Put(&worker.Task{
Ack: ack,
Do: srv.Wrap(srv.FinishUpload),
Res: res,
Req: req,
})
if !ok {
srv.Http.Fill(httpUtil.Err503, res)
}
execErr := srv.WorkerPool.IsInTime(ack, time.Duration(srv.Conf.Timeout)*time.Millisecond)
if srv.Err.IsErr(execErr) {
srv.Http.Fill(httpUtil.Err500, res)
}
}
func (srv *SrvShare) StartUpload(res http.ResponseWriter, req *http.Request) interface{} {
return srv.startUpload(req.FormValue(srv.Conf.KeyFileName))
}
func (srv *SrvShare) startUpload(fileName string) interface{} {
if !IsValidFileName(fileName) {
return httpUtil.Err400
}
id := DefaultId
if srv.Conf.Production {
id = genInfoId(fileName, srv.Conf.SecretKeyByte)
}
info := &fileidx.FileInfo{
Id: id,
DownLimit: srv.Conf.DownLimit,
ModTime: time.Now().UnixNano(),
PathLocal: fileName,
Uploaded: 0,
State: fileidx.StateStarted,
}
switch srv.Index.Add(info) {
case 0:
// go on
case -1:
return httpUtil.Err412
case 1:
return httpUtil.Err500 // TODO: use correct status code
default:
srv.Index.Del(id)
return httpUtil.Err500
}
fullPath := filepath.Join(srv.Conf.PathLocal, info.PathLocal)
createFileErr := srv.Fs.CreateFile(fullPath)
switch {
case createFileErr == fsutil.ErrExists:
srv.Index.Del(id)
return httpUtil.Err412
case createFileErr == fsutil.ErrUnknown:
srv.Index.Del(id)
return httpUtil.Err500
default:
srv.Index.SetState(id, fileidx.StateUploading)
return &ByteRange{
ShareId: id,
Start: 0,
Length: srv.Conf.MaxUpBytesPerSec,
}
}
}
func (srv *SrvShare) Upload(res http.ResponseWriter, req *http.Request) interface{} {
shareId := req.FormValue(srv.Conf.KeyShareId)
start, startErr := strconv.ParseInt(req.FormValue(srv.Conf.KeyStart), 10, 64)
length, lengthErr := strconv.ParseInt(req.FormValue(srv.Conf.KeyLen), 10, 64)
chunk, _, chunkErr := req.FormFile(srv.Conf.KeyChunk)
if srv.Err.IsErr(startErr) ||
srv.Err.IsErr(lengthErr) ||
srv.Err.IsErr(chunkErr) {
return httpUtil.Err400
}
return srv.upload(shareId, start, length, chunk)
}
func (srv *SrvShare) upload(shareId string, start int64, length int64, chunk io.Reader) interface{} {
if !srv.IsValidShareId(shareId) {
return httpUtil.Err400
}
fileInfo, found := srv.Index.Get(shareId)
if !found {
return httpUtil.Err404
}
if !srv.IsValidStart(start, fileInfo.Uploaded) || !srv.IsValidLength(length) {
return httpUtil.Err400
}
fullPath := filepath.Join(srv.Conf.PathLocal, fileInfo.PathLocal)
if !srv.Fs.CopyChunkN(fullPath, chunk, start, length) {
return httpUtil.Err500
}
if srv.Index.IncrUploaded(shareId, length) == 0 {
return httpUtil.Err404
}
return &ByteRange{
ShareId: shareId,
Start: start + length,
Length: srv.Conf.MaxUpBytesPerSec,
}
}
func (srv *SrvShare) FinishUpload(res http.ResponseWriter, req *http.Request) interface{} {
shareId := req.FormValue(srv.Conf.KeyShareId)
return srv.finishUpload(shareId)
}
func (srv *SrvShare) finishUpload(shareId string) interface{} {
if !srv.Index.SetState(shareId, fileidx.StateDone) {
return httpUtil.Err404
}
return &ShareInfo{
ShareId: shareId,
}
}
func genInfoId(content string, key []byte) string {
encrypter := encrypt.HmacEncryptor{Key: key}
return encrypter.Encrypt([]byte(content))
}

368
server/apis/upload_test.go Normal file
View file

@ -0,0 +1,368 @@
package apis
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
import (
"quickshare/server/libs/cfg"
"quickshare/server/libs/encrypt"
"quickshare/server/libs/errutil"
"quickshare/server/libs/fileidx"
"quickshare/server/libs/httputil"
"quickshare/server/libs/httpworker"
"quickshare/server/libs/limiter"
"quickshare/server/libs/logutil"
"quickshare/server/libs/walls"
)
const testCap = 3
func initServiceForUploadTest(config *cfg.Config, indexMap map[string]*fileidx.FileInfo) *SrvShare {
logger := logutil.NewSlog(os.Stdout, config.AppName)
setLog := func(srv *SrvShare) {
srv.Log = logger
}
setWorkerPool := func(srv *SrvShare) {
workerPoolSize := config.WorkerPoolSize
taskQueueSize := config.TaskQueueSize
srv.WorkerPool = httpworker.NewWorkerPool(workerPoolSize, taskQueueSize, logger)
}
setWalls := func(srv *SrvShare) {
encrypterMaker := encrypt.JwtEncrypterMaker
ipLimiter := limiter.NewRateLimiter(config.LimiterCap, config.LimiterTtl, config.LimiterCyc, config.BucketCap, map[int16]int16{})
opLimiter := limiter.NewRateLimiter(config.LimiterCap, config.LimiterTtl, config.LimiterCyc, config.BucketCap, map[int16]int16{})
srv.Walls = walls.NewAccessWalls(config, ipLimiter, opLimiter, encrypterMaker)
}
setIndex := func(srv *SrvShare) {
srv.Index = fileidx.NewMemFileIndexWithMap(len(indexMap)+testCap, indexMap)
}
setFs := func(srv *SrvShare) {
srv.Fs = &stubFsUtil{}
}
setEncryptor := func(srv *SrvShare) {
srv.Encryptor = &encrypt.HmacEncryptor{Key: config.SecretKeyByte}
}
errChecker := errutil.NewErrChecker(!config.Production, logger)
setErr := func(srv *SrvShare) {
srv.Err = errChecker
}
return InitSrvShare(config, setIndex, setWalls, setWorkerPool, setFs, setEncryptor, setLog, setErr)
}
func TestStartUpload(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
FileName string
}
type Output struct {
Response interface{}
IndexMap map[string]*fileidx.FileInfo
}
type testCase struct {
Desc string
Init
Input
Output
}
testCases := []testCase{
testCase{
Desc: "invalid file name",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
FileName: "",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err400,
},
},
testCase{
Desc: "succeed to start uploading",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
FileName: "filename",
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
DefaultId: &fileidx.FileInfo{
Id: DefaultId,
DownLimit: conf.DownLimit,
ModTime: time.Now().UnixNano(),
PathLocal: "filename",
Uploaded: 0,
State: fileidx.StateUploading,
},
},
Response: &ByteRange{
ShareId: DefaultId,
Start: 0,
Length: conf.MaxUpBytesPerSec,
},
},
},
}
for _, testCase := range testCases {
srv := initServiceForUploadTest(conf, testCase.Init.IndexMap)
// verify CreateFile
expectCreateFileName = filepath.Join(conf.PathLocal, testCase.FileName)
response := srv.startUpload(testCase.FileName)
// verify index
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf("startUpload: index not equal got: %v, %v, expect: %v", srv.Index.List(), response, testCase.Output.IndexMap)
}
// verify response
switch expectRes := testCase.Output.Response.(type) {
case *ByteRange:
res := response.(*ByteRange)
if res.ShareId != expectRes.ShareId ||
res.Start != expectRes.Start ||
res.Length != expectRes.Length {
t.Fatalf(fmt.Sprintf("startUpload: res=%v expect=%v", res, expectRes))
}
case httputil.MsgRes:
if response != expectRes {
t.Fatalf(fmt.Sprintf("startUpload: reponse=%v expectRes=%v", response, expectRes))
}
default:
t.Fatalf(fmt.Sprintf("startUpload: type not found: %T %T", testCase.Output.Response, httputil.Err400))
}
}
}
func TestUpload(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
Start int64
Len int64
Chunk io.Reader
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response interface{}
}
type testCase struct {
Desc string
Init
Input
Output
}
testCases := []testCase{
testCase{
Desc: "shareid does not exist",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
ShareId: DefaultId,
Start: 0,
Len: 1,
Chunk: strings.NewReader(""),
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err404,
},
},
testCase{
Desc: "succeed",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
DefaultId: &fileidx.FileInfo{
Id: DefaultId,
DownLimit: conf.MaxShares,
PathLocal: "path/filename",
State: fileidx.StateUploading,
Uploaded: 0,
},
},
},
Input: Input{
ShareId: DefaultId,
Start: 0,
Len: 1,
Chunk: strings.NewReader("a"),
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
DefaultId: &fileidx.FileInfo{
Id: DefaultId,
DownLimit: conf.MaxShares,
PathLocal: "path/filename",
State: fileidx.StateUploading,
Uploaded: 1,
},
},
Response: &ByteRange{
ShareId: DefaultId,
Start: 1,
Length: conf.MaxUpBytesPerSec,
},
},
},
}
for _, testCase := range testCases {
srv := initServiceForUploadTest(conf, testCase.Init.IndexMap)
response := srv.upload(
testCase.Input.ShareId,
testCase.Input.Start,
testCase.Input.Len,
testCase.Input.Chunk,
)
// TODO: not verified copyChunk
// verify index
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf("upload: index not identical got: %v want: %v", srv.Index.List(), testCase.Output.IndexMap)
}
// verify response
switch response.(type) {
case *ByteRange:
br := testCase.Output.Response.(*ByteRange)
res := response.(*ByteRange)
if res.ShareId != br.ShareId || res.Start != br.Start || res.Length != br.Length {
t.Fatalf(fmt.Sprintf("upload: response=%v expectRes=%v", res, br))
}
default:
if response != testCase.Output.Response {
t.Fatalf(fmt.Sprintf("upload: response=%v expectRes=%v", response, testCase.Output.Response))
}
}
}
}
func TestFinishUpload(t *testing.T) {
conf := cfg.NewConfig()
conf.Production = false
type Init struct {
IndexMap map[string]*fileidx.FileInfo
}
type Input struct {
ShareId string
Start int64
Len int64
Chunk io.Reader
}
type Output struct {
IndexMap map[string]*fileidx.FileInfo
Response interface{}
}
type testCase struct {
Desc string
Init
Input
Output
}
testCases := []testCase{
testCase{
Desc: "success",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{
DefaultId: &fileidx.FileInfo{
Id: DefaultId,
DownLimit: conf.MaxShares,
PathLocal: "path/filename",
State: fileidx.StateUploading,
Uploaded: 1,
},
},
},
Input: Input{
ShareId: DefaultId,
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{
DefaultId: &fileidx.FileInfo{
Id: DefaultId,
DownLimit: conf.MaxShares,
PathLocal: "path/filename",
State: fileidx.StateDone,
Uploaded: 1,
},
},
Response: &ShareInfo{
ShareId: DefaultId,
},
},
},
testCase{
Desc: "shareId exists",
Init: Init{
IndexMap: map[string]*fileidx.FileInfo{},
},
Input: Input{
ShareId: DefaultId,
},
Output: Output{
IndexMap: map[string]*fileidx.FileInfo{},
Response: httputil.Err404,
},
},
}
for _, testCase := range testCases {
srv := initServiceForUploadTest(conf, testCase.Init.IndexMap)
response := srv.finishUpload(testCase.ShareId)
if !sameMap(srv.Index.List(), testCase.Output.IndexMap) {
t.Fatalf("finishUpload: index not identical got: %v, want: %v", srv.Index.List(), testCase.Output.IndexMap)
}
switch res := response.(type) {
case httputil.MsgRes:
expectRes := testCase.Output.Response.(httputil.MsgRes)
if res != expectRes {
t.Fatalf(fmt.Sprintf("finishUpload: reponse=%v expectRes=%v", res, expectRes))
}
case *ShareInfo:
info, found := testCase.Output.IndexMap[res.ShareId]
if !found || info.State != fileidx.StateDone {
// TODO: should use isValidUrl or better to verify result
t.Fatalf(fmt.Sprintf("finishUpload: share info is not correct: received: %v expect: %v", res.ShareId, testCase.ShareId))
}
default:
t.Fatalf(fmt.Sprintf("finishUpload: type not found: %T %T", response, testCase.Output.Response))
}
}
}