package main import ( "archive/zip" "bytes" "context" "crypto/sha1" "encoding/hex" "encoding/json" "errors" "fmt" "io" "io/ioutil" "mime" "mime/multipart" "net" "net/http" "net/url" "os" "path/filepath" "runtime/debug" "strconv" "strings" "sync" "time" "unicode/utf8" "database/sql" "math/rand" "sort" "syscall" "github.com/haiwen/seafile-server/fileserver/blockmgr" "github.com/haiwen/seafile-server/fileserver/commitmgr" "github.com/haiwen/seafile-server/fileserver/diff" "github.com/haiwen/seafile-server/fileserver/fsmgr" "github.com/haiwen/seafile-server/fileserver/option" "github.com/haiwen/seafile-server/fileserver/repomgr" log "github.com/sirupsen/logrus" "golang.org/x/text/unicode/norm" ) const ( cacheBlockMapThreshold = 1 << 23 blockMapCacheExpiretime int64 = 3600 * 24 fileopCleaningIntervalSec = 3600 duplicateNamesCount = 1000 ) var blockMapCacheTable sync.Map // Dirents is an alias for slice of SeafDirent. type Dirents []*fsmgr.SeafDirent func (d Dirents) Less(i, j int) bool { return d[i].Name > d[j].Name } func (d Dirents) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d Dirents) Len() int { return len(d) } func fileopInit() { ticker := time.NewTicker(time.Second * fileopCleaningIntervalSec) go RecoverWrapper(func() { for range ticker.C { removeFileopExpireCache() } }) } func initUpload() { objDir := filepath.Join(dataDir, "httptemp", "cluster-shared") os.MkdirAll(objDir, os.ModePerm) } // contentType = "application/octet-stream" func parseContentType(fileName string) string { var contentType string parts := strings.Split(fileName, ".") if len(parts) >= 2 { suffix := parts[len(parts)-1] switch suffix { case "txt": contentType = "text/plain" case "doc": contentType = "application/vnd.ms-word" case "docx": contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" case "ppt": contentType = "application/vnd.ms-powerpoint" case "xls": contentType = "application/vnd.ms-excel" case "xlsx": contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" case "pdf": contentType = "application/pdf" case "zip": contentType = "application/zip" case "mp3": contentType = "audio/mp3" case "mpeg": contentType = "video/mpeg" case "mp4": contentType = "video/mp4" case "jpeg", "JPEG", "jpg", "JPG": contentType = "image/jpeg" case "png", "PNG": contentType = "image/png" case "gif", "GIF": contentType = "image/gif" case "svg", "SVG": contentType = "image/svg+xml" } } return contentType } func accessCB(rsp http.ResponseWriter, r *http.Request) *appError { parts := strings.Split(r.URL.Path[1:], "/") if len(parts) < 3 { msg := "Invalid URL" return &appError{nil, msg, http.StatusBadRequest} } token := parts[1] fileName := parts[2] accessInfo, err := parseWebaccessInfo(token) if err != nil { return err } repoID := accessInfo.repoID op := accessInfo.op user := accessInfo.user objID := accessInfo.objID if op != "view" && op != "download" && op != "download-link" { msg := "Operation does not match access token." return &appError{nil, msg, http.StatusForbidden} } if _, ok := r.Header["If-Modified-Since"]; ok { return &appError{nil, "", http.StatusNotModified} } now := time.Now() rsp.Header().Set("Last-Modified", now.Format("Mon, 2 Jan 2006 15:04:05 GMT")) rsp.Header().Set("Cache-Control", "max-age=3600") ranges := r.Header["Range"] byteRanges := strings.Join(ranges, "") repo := repomgr.Get(repoID) if repo == nil { msg := "Bad repo id" return &appError{nil, msg, http.StatusBadRequest} } var cryptKey *seafileCrypt if repo.IsEncrypted { key, err := parseCryptKey(rsp, repoID, user, repo.EncVersion) if err != nil { return err } cryptKey = key } exists, _ := fsmgr.Exists(repo.StoreID, objID) if !exists { msg := "Invalid file id" return &appError{nil, msg, http.StatusBadRequest} } if !repo.IsEncrypted && len(byteRanges) != 0 { if err := doFileRange(rsp, r, repo, objID, fileName, op, byteRanges, user); err != nil { return err } } else if err := doFile(rsp, r, repo, objID, fileName, op, cryptKey, user); err != nil { return err } return nil } func parseCryptKey(rsp http.ResponseWriter, repoID string, user string, version int) (*seafileCrypt, *appError) { key, err := rpcclient.Call("seafile_get_decrypt_key", repoID, user) if err != nil { errMessage := "Repo is encrypted. Please provide password to view it." return nil, &appError{nil, errMessage, http.StatusBadRequest} } cryptKey, ok := key.(map[string]interface{}) if !ok { err := fmt.Errorf("failed to assert crypt key") return nil, &appError{err, "", http.StatusInternalServerError} } seafileKey := new(seafileCrypt) seafileKey.version = version if cryptKey != nil { key, ok := cryptKey["key"].(string) if !ok { err := fmt.Errorf("failed to parse crypt key") return nil, &appError{err, "", http.StatusInternalServerError} } iv, ok := cryptKey["iv"].(string) if !ok { err := fmt.Errorf("failed to parse crypt iv") return nil, &appError{err, "", http.StatusInternalServerError} } seafileKey.key, err = hex.DecodeString(key) if err != nil { err := fmt.Errorf("failed to decode key: %v", err) return nil, &appError{err, "", http.StatusInternalServerError} } seafileKey.iv, err = hex.DecodeString(iv) if err != nil { err := fmt.Errorf("failed to decode iv: %v", err) return nil, &appError{err, "", http.StatusInternalServerError} } } return seafileKey, nil } func doFile(rsp http.ResponseWriter, r *http.Request, repo *repomgr.Repo, fileID string, fileName string, operation string, cryptKey *seafileCrypt, user string) *appError { file, err := fsmgr.GetSeafile(repo.StoreID, fileID) if err != nil { msg := "Failed to get seafile" return &appError{nil, msg, http.StatusBadRequest} } rsp.Header().Set("Access-Control-Allow-Origin", "*") setCommonHeaders(rsp, r, operation, fileName) //filesize string fileSize := fmt.Sprintf("%d", file.FileSize) rsp.Header().Set("Content-Length", fileSize) if r.Method == "HEAD" { rsp.WriteHeader(http.StatusOK) return nil } if file.FileSize == 0 { rsp.WriteHeader(http.StatusOK) return nil } if cryptKey != nil { for _, blkID := range file.BlkIDs { var buf bytes.Buffer blockmgr.Read(repo.StoreID, blkID, &buf) decoded, err := cryptKey.decrypt(buf.Bytes()) if err != nil { err := fmt.Errorf("failed to decrypt block %s: %v", blkID, err) return &appError{err, "", http.StatusInternalServerError} } _, err = rsp.Write(decoded) if err != nil { return nil } } return nil } for _, blkID := range file.BlkIDs { err := blockmgr.Read(repo.StoreID, blkID, rsp) if err != nil { if !isNetworkErr(err) { log.Printf("failed to read block %s: %v", blkID, err) } return nil } } if operation != "view" { oper := "web-file-download" if operation == "download-link" { oper = "link-file-download" } sendStatisticMsg(repo.StoreID, user, oper, file.FileSize) } return nil } func isNetworkErr(err error) bool { _, ok := err.(net.Error) if ok { return true } return false } type blockMap struct { blkSize []uint64 expireTime int64 } func doFileRange(rsp http.ResponseWriter, r *http.Request, repo *repomgr.Repo, fileID string, fileName string, operation string, byteRanges string, user string) *appError { file, err := fsmgr.GetSeafile(repo.StoreID, fileID) if err != nil { msg := "Failed to get seafile" return &appError{nil, msg, http.StatusBadRequest} } if file.FileSize == 0 { rsp.WriteHeader(http.StatusOK) return nil } start, end, ok := parseRange(byteRanges, file.FileSize) if !ok { conRange := fmt.Sprintf("bytes */%d", file.FileSize) rsp.Header().Set("Content-Range", conRange) return &appError{nil, "", http.StatusRequestedRangeNotSatisfiable} } rsp.Header().Set("Accept-Ranges", "bytes") setCommonHeaders(rsp, r, operation, fileName) //filesize string conLen := fmt.Sprintf("%d", end-start+1) rsp.Header().Set("Content-Length", conLen) conRange := fmt.Sprintf("bytes %d-%d/%d", start, end, file.FileSize) rsp.Header().Set("Content-Range", conRange) rsp.WriteHeader(http.StatusPartialContent) var blkSize []uint64 if file.FileSize > cacheBlockMapThreshold { if v, ok := blockMapCacheTable.Load(file.FileID); ok { if blkMap, ok := v.(*blockMap); ok { blkSize = blkMap.blkSize } } if len(blkSize) == 0 { for _, v := range file.BlkIDs { size, err := blockmgr.Stat(repo.StoreID, v) if err != nil { err := fmt.Errorf("failed to stat block %s : %v", v, err) return &appError{err, "", http.StatusInternalServerError} } blkSize = append(blkSize, uint64(size)) } blockMapCacheTable.Store(file.FileID, &blockMap{blkSize, time.Now().Unix() + blockMapCacheExpiretime}) } } else { for _, v := range file.BlkIDs { size, err := blockmgr.Stat(repo.StoreID, v) if err != nil { err := fmt.Errorf("failed to stat block %s : %v", v, err) return &appError{err, "", http.StatusInternalServerError} } blkSize = append(blkSize, uint64(size)) } } var off uint64 var pos uint64 var startBlock int for i, v := range blkSize { pos = start - off off += v if off > start { startBlock = i break } } // Read block from the start block and specified position var i int for ; i < len(file.BlkIDs); i++ { if i < startBlock { continue } blkID := file.BlkIDs[i] var buf bytes.Buffer if end-start+1 <= blkSize[i]-pos { err := blockmgr.Read(repo.StoreID, blkID, &buf) if err != nil { if !isNetworkErr(err) { log.Printf("failed to read block %s: %v", blkID, err) } return nil } recvBuf := buf.Bytes() rsp.Write(recvBuf[pos : pos+end-start+1]) return nil } err := blockmgr.Read(repo.StoreID, blkID, &buf) if err != nil { if !isNetworkErr(err) { log.Printf("failed to read block %s: %v", blkID, err) } return nil } recvBuf := buf.Bytes() _, err = rsp.Write(recvBuf[pos:]) if err != nil { return nil } start += blkSize[i] - pos i++ break } // Always read block from the remaining block and pos=0 for ; i < len(file.BlkIDs); i++ { blkID := file.BlkIDs[i] var buf bytes.Buffer if end-start+1 <= blkSize[i] { err := blockmgr.Read(repo.StoreID, blkID, &buf) if err != nil { if !isNetworkErr(err) { log.Printf("failed to read block %s: %v", blkID, err) } return nil } recvBuf := buf.Bytes() _, err = rsp.Write(recvBuf[:end-start+1]) if err != nil { return nil } break } else { err := blockmgr.Read(repo.StoreID, blkID, rsp) if err != nil { if !isNetworkErr(err) { log.Printf("failed to read block %s: %v", blkID, err) } return nil } start += blkSize[i] } } oper := "web-file-download" if operation == "download-link" { oper = "link-file-download" } sendStatisticMsg(repo.StoreID, user, oper, file.FileSize) return nil } func parseRange(byteRanges string, fileSize uint64) (uint64, uint64, bool) { start := strings.Index(byteRanges, "=") end := strings.Index(byteRanges, "-") if end < 0 { return 0, 0, false } var startByte, endByte uint64 if start+1 == end { retByte, err := strconv.ParseUint(byteRanges[end+1:], 10, 64) if err != nil || retByte == 0 { return 0, 0, false } startByte = fileSize - retByte endByte = fileSize - 1 } else if end+1 == len(byteRanges) { firstByte, err := strconv.ParseUint(byteRanges[start+1:end], 10, 64) if err != nil { return 0, 0, false } startByte = firstByte endByte = fileSize - 1 } else { firstByte, err := strconv.ParseUint(byteRanges[start+1:end], 10, 64) if err != nil { return 0, 0, false } lastByte, err := strconv.ParseUint(byteRanges[end+1:], 10, 64) if err != nil { return 0, 0, false } if lastByte > fileSize-1 { lastByte = fileSize - 1 } startByte = firstByte endByte = lastByte } if startByte > endByte { return 0, 0, false } return startByte, endByte, true } func setCommonHeaders(rsp http.ResponseWriter, r *http.Request, operation, fileName string) { fileType := parseContentType(fileName) if fileType != "" { var contentType string if strings.Index(fileType, "text") != -1 { contentType = fileType + "; " + "charset=gbk" } else { contentType = fileType } rsp.Header().Set("Content-Type", contentType) } else { rsp.Header().Set("Content-Type", "application/octet-stream") } var contFileName string if operation == "download" || operation == "download-link" || operation == "downloadblks" { // Since the file name downloaded by safari will be garbled, we need to encode the filename. // Safari cannot parse unencoded utf8 characters. contFileName = fmt.Sprintf("attachment;filename*=\"utf-8' '%s\"", url.PathEscape(fileName)) } else { contFileName = fmt.Sprintf("inline;filename*=\"utf-8' '%s\"", url.PathEscape(fileName)) } rsp.Header().Set("Content-Disposition", contFileName) if fileType != "image/jpg" { rsp.Header().Set("X-Content-Type-Options", "nosniff") } } func accessBlksCB(rsp http.ResponseWriter, r *http.Request) *appError { parts := strings.Split(r.URL.Path[1:], "/") if len(parts) < 3 { msg := "Invalid URL" return &appError{nil, msg, http.StatusBadRequest} } token := parts[1] blkID := parts[2] accessInfo, err := parseWebaccessInfo(token) if err != nil { return err } repoID := accessInfo.repoID op := accessInfo.op user := accessInfo.user id := accessInfo.objID if _, ok := r.Header["If-Modified-Since"]; ok { return &appError{nil, "", http.StatusNotModified} } now := time.Now() rsp.Header().Set("Last-Modified", now.Format("Mon, 2 Jan 2006 15:04:05 GMT")) rsp.Header().Set("Cache-Control", "max-age=3600") repo := repomgr.Get(repoID) if repo == nil { msg := "Bad repo id" return &appError{nil, msg, http.StatusBadRequest} } exists, _ := fsmgr.Exists(repo.StoreID, id) if !exists { msg := "Invalid file id" return &appError{nil, msg, http.StatusBadRequest} } if op != "downloadblks" { msg := "Operation does not match access token" return &appError{nil, msg, http.StatusForbidden} } if err := doBlock(rsp, r, repo, id, user, blkID); err != nil { return err } return nil } func doBlock(rsp http.ResponseWriter, r *http.Request, repo *repomgr.Repo, fileID string, user string, blkID string) *appError { file, err := fsmgr.GetSeafile(repo.StoreID, fileID) if err != nil { msg := "Failed to get seafile" return &appError{nil, msg, http.StatusBadRequest} } var found bool for _, id := range file.BlkIDs { if id == blkID { found = true break } } if !found { rsp.WriteHeader(http.StatusBadRequest) return nil } exists := blockmgr.Exists(repo.StoreID, blkID) if !exists { rsp.WriteHeader(http.StatusBadRequest) return nil } rsp.Header().Set("Access-Control-Allow-Origin", "*") setCommonHeaders(rsp, r, "downloadblks", blkID) size, err := blockmgr.Stat(repo.StoreID, blkID) if err != nil { msg := "Failed to stat block" return &appError{nil, msg, http.StatusBadRequest} } if size == 0 { rsp.WriteHeader(http.StatusOK) return nil } fileSize := fmt.Sprintf("%d", size) rsp.Header().Set("Content-Length", fileSize) err = blockmgr.Read(repo.StoreID, blkID, rsp) if err != nil { if !isNetworkErr(err) { log.Printf("failed to read block %s: %v", blkID, err) } } sendStatisticMsg(repo.StoreID, user, "web-file-download", uint64(size)) return nil } func accessZipCB(rsp http.ResponseWriter, r *http.Request) *appError { parts := strings.Split(r.URL.Path[1:], "/") if len(parts) != 2 { msg := "Invalid URL" return &appError{nil, msg, http.StatusBadRequest} } token := parts[1] accessInfo, err := parseWebaccessInfo(token) if err != nil { return err } repoID := accessInfo.repoID op := accessInfo.op user := accessInfo.user data := accessInfo.objID if op != "download-dir" && op != "download-dir-link" && op != "download-multi" && op != "download-multi-link" { msg := "Operation does not match access token" return &appError{nil, msg, http.StatusForbidden} } if _, ok := r.Header["If-Modified-Since"]; ok { return &appError{nil, "", http.StatusNotModified} } now := time.Now() rsp.Header().Set("Last-Modified", now.Format("Mon, 2 Jan 2006 15:04:05 GMT")) rsp.Header().Set("Cache-Control", "max-age=3600") if err := downloadZipFile(rsp, r, data, repoID, user, op); err != nil { return err } return nil } func downloadZipFile(rsp http.ResponseWriter, r *http.Request, data, repoID, user, op string) *appError { repo := repomgr.Get(repoID) if repo == nil { msg := "Failed to get repo" return &appError{nil, msg, http.StatusBadRequest} } var cryptKey *seafileCrypt if repo.IsEncrypted { key, err := parseCryptKey(rsp, repoID, user, repo.EncVersion) if err != nil { return err } cryptKey = key } obj := make(map[string]interface{}) err := json.Unmarshal([]byte(data), &obj) if err != nil { err := fmt.Errorf("failed to parse obj data for zip: %v", err) return &appError{err, "", http.StatusInternalServerError} } ar := zip.NewWriter(rsp) defer ar.Close() if op == "download-dir" || op == "download-dir-link" { dirName, ok := obj["dir_name"].(string) if !ok || dirName == "" { err := fmt.Errorf("invalid download dir data: miss dir_name field") return &appError{err, "", http.StatusInternalServerError} } objID, ok := obj["obj_id"].(string) if !ok || objID == "" { err := fmt.Errorf("invalid download dir data: miss obj_id field") return &appError{err, "", http.StatusInternalServerError} } zipName := dirName + ".zip" setCommonHeaders(rsp, r, "download", zipName) // The zip name downloaded by safari will be garbled if we encode the zip name, // because we download zip file using chunk encoding. contFileName := fmt.Sprintf("attachment;filename=\"%s\"", zipName) rsp.Header().Set("Content-Disposition", contFileName) rsp.Header().Set("Content-Type", "application/octet-stream") err := packDir(ar, repo, objID, dirName, cryptKey) if err != nil { log.Printf("failed to pack dir %s: %v", dirName, err) return nil } } else { dirList, err := parseDirFilelist(repo, obj) if err != nil { return &appError{err, "", http.StatusInternalServerError} } now := time.Now() zipName := fmt.Sprintf("documents-export-%d-%d-%d.zip", now.Year(), now.Month(), now.Day()) setCommonHeaders(rsp, r, "download", zipName) contFileName := fmt.Sprintf("attachment;filename=\"%s\"", zipName) rsp.Header().Set("Content-Disposition", contFileName) rsp.Header().Set("Content-Type", "application/octet-stream") for _, v := range dirList { if fsmgr.IsDir(v.Mode) { if err := packDir(ar, repo, v.ID, v.Name, cryptKey); err != nil { if !isNetworkErr(err) { log.Printf("failed to pack dir %s: %v", v.Name, err) } return nil } } else { if err := packFiles(ar, &v, repo, "", cryptKey); err != nil { if !isNetworkErr(err) { log.Printf("failed to pack file %s: %v", v.Name, err) } return nil } } } } return nil } func parseDirFilelist(repo *repomgr.Repo, obj map[string]interface{}) ([]fsmgr.SeafDirent, error) { parentDir, ok := obj["parent_dir"].(string) if !ok || parentDir == "" { err := fmt.Errorf("invalid download multi data, miss parent_dir field") return nil, err } dir, err := fsmgr.GetSeafdirByPath(repo.StoreID, repo.RootID, parentDir) if err != nil { err := fmt.Errorf("failed to get dir %s repo %s", parentDir, repo.StoreID) return nil, err } fileList, ok := obj["file_list"].([]interface{}) if !ok || fileList == nil { err := fmt.Errorf("invalid download multi data, miss file_list field") return nil, err } direntHash := make(map[string]fsmgr.SeafDirent) for _, v := range dir.Entries { direntHash[v.Name] = *v } direntList := make([]fsmgr.SeafDirent, 0) for _, fileName := range fileList { name, ok := fileName.(string) if !ok { err := fmt.Errorf("invalid download multi data") return nil, err } v, ok := direntHash[name] if !ok { err := fmt.Errorf("invalid download multi data") return nil, err } direntList = append(direntList, v) } return direntList, nil } func packDir(ar *zip.Writer, repo *repomgr.Repo, dirID, dirPath string, cryptKey *seafileCrypt) error { dirent, err := fsmgr.GetSeafdir(repo.StoreID, dirID) if err != nil { err := fmt.Errorf("failed to get dir for zip: %v", err) return err } if dirent.Entries == nil { fileDir := filepath.Join(dirPath) fileDir = strings.TrimLeft(fileDir, "/") _, err := ar.Create(fileDir + "/") if err != nil { err := fmt.Errorf("failed to create zip dir: %v", err) return err } return nil } entries := dirent.Entries for _, v := range entries { fileDir := filepath.Join(dirPath, v.Name) fileDir = strings.TrimLeft(fileDir, "/") if fsmgr.IsDir(v.Mode) { if err := packDir(ar, repo, v.ID, fileDir, cryptKey); err != nil { return err } } else { if err := packFiles(ar, v, repo, dirPath, cryptKey); err != nil { return err } } } return nil } func packFiles(ar *zip.Writer, dirent *fsmgr.SeafDirent, repo *repomgr.Repo, parentPath string, cryptKey *seafileCrypt) error { file, err := fsmgr.GetSeafile(repo.StoreID, dirent.ID) if err != nil { err := fmt.Errorf("failed to get seafile : %v", err) return err } filePath := filepath.Join(parentPath, dirent.Name) filePath = strings.TrimLeft(filePath, "/") fileHeader := new(zip.FileHeader) fileHeader.Name = filePath fileHeader.Modified = time.Unix(dirent.Mtime, 0) fileHeader.Method = zip.Deflate zipFile, err := ar.CreateHeader(fileHeader) if err != nil { err := fmt.Errorf("failed to create zip file : %v", err) return err } if cryptKey != nil { for _, blkID := range file.BlkIDs { var buf bytes.Buffer blockmgr.Read(repo.StoreID, blkID, &buf) decoded, err := cryptKey.decrypt(buf.Bytes()) if err != nil { err := fmt.Errorf("failed to decrypt block %s: %v", blkID, err) return err } _, err = zipFile.Write(decoded) if err != nil { return err } } return nil } for _, blkID := range file.BlkIDs { err := blockmgr.Read(repo.StoreID, blkID, zipFile) if err != nil { return err } } return nil } type recvData struct { parentDir string tokenType string repoID string user string rstart int64 rend int64 fsize int64 fileNames []string files []string fileHeaders []*multipart.FileHeader } func uploadAPICB(rsp http.ResponseWriter, r *http.Request) *appError { if r.Method == "OPTIONS" { setAccessControl(rsp) rsp.WriteHeader(http.StatusOK) return nil } fsm, err := parseUploadHeaders(r) if err != nil { return err } if err := doUpload(rsp, r, fsm, false); err != nil { formatJSONError(rsp, err) return err } return nil } func setAccessControl(rsp http.ResponseWriter) { rsp.Header().Set("Access-Control-Allow-Origin", "*") rsp.Header().Set("Access-Control-Allow-Headers", "x-requested-with, content-type, content-range, content-disposition, accept, origin, authorization") rsp.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") rsp.Header().Set("Access-Control-Max-Age", "86400") } func uploadAjaxCB(rsp http.ResponseWriter, r *http.Request) *appError { if r.Method == "OPTIONS" { setAccessControl(rsp) rsp.WriteHeader(http.StatusOK) return nil } fsm, err := parseUploadHeaders(r) if err != nil { return err } if err := doUpload(rsp, r, fsm, true); err != nil { formatJSONError(rsp, err) return err } return nil } func formatJSONError(rsp http.ResponseWriter, err *appError) { if err.Message != "" { rsp.Header().Set("Content-Type", "application/json; charset=utf-8") err.Message = fmt.Sprintf("{\"error\": \"%s\"}", err.Message) } } func normalizeUTF8Path(p string) string { newPath := norm.NFC.Bytes([]byte(p)) return string(newPath) } func doUpload(rsp http.ResponseWriter, r *http.Request, fsm *recvData, isAjax bool) *appError { setAccessControl(rsp) if err := r.ParseMultipartForm(1 << 20); err != nil { return &appError{nil, "", http.StatusBadRequest} } defer r.MultipartForm.RemoveAll() repoID := fsm.repoID user := fsm.user replaceStr := r.FormValue("replace") var replaceExisted bool if replaceStr != "" { replace, err := strconv.ParseInt(replaceStr, 10, 64) if err != nil || (replace != 0 && replace != 1) { msg := "Invalid argument replace.\n" return &appError{nil, msg, http.StatusBadRequest} } if replace == 1 { replaceExisted = true } } parentDir := normalizeUTF8Path(r.FormValue("parent_dir")) if parentDir == "" { msg := "No parent_dir given.\n" return &appError{nil, msg, http.StatusBadRequest} } relativePath := normalizeUTF8Path(r.FormValue("relative_path")) if relativePath != "" { if relativePath[0] == '/' || relativePath[0] == '\\' { msg := "Invalid relative path" return &appError{nil, msg, http.StatusBadRequest} } } newParentDir := filepath.Join("/", parentDir, relativePath) defer clearTmpFile(fsm, newParentDir) if fsm.rstart >= 0 { if parentDir[0] != '/' { msg := "Invalid parent dir" return &appError{nil, msg, http.StatusBadRequest} } formFiles := r.MultipartForm.File files, ok := formFiles["file"] if !ok { msg := "No file in multipart form.\n" return &appError{nil, msg, http.StatusBadRequest} } if len(files) > 1 { msg := "More files in one request" return &appError{nil, msg, http.StatusBadRequest} } err := writeBlockDataToTmpFile(r, fsm, formFiles, repoID, newParentDir) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to write block data to tmp file: %v", err) return &appError{err, msg, http.StatusInternalServerError} } if fsm.rend != fsm.fsize-1 { rsp.Header().Set("Content-Type", "application/json; charset=utf-8") success := "{\"success\": true}" _, err := rsp.Write([]byte(success)) if err != nil { log.Printf("failed to write data to response") } return nil } } else { formFiles := r.MultipartForm.File fileHeaders, ok := formFiles["file"] if !ok { msg := "No file in multipart form.\n" return &appError{nil, msg, http.StatusBadRequest} } for _, handler := range fileHeaders { fileName := filepath.Base(handler.Filename) fsm.fileNames = append(fsm.fileNames, normalizeUTF8Path(fileName)) fsm.fileHeaders = append(fsm.fileHeaders, handler) } } if fsm.fileNames == nil { msg := "No file uploaded.\n" return &appError{nil, msg, http.StatusBadRequest} } if err := checkParentDir(repoID, parentDir); err != nil { return err } if !isParentMatched(fsm.parentDir, parentDir) { msg := "Parent dir doesn't match." return &appError{nil, msg, http.StatusForbidden} } if err := checkTmpFileList(fsm); err != nil { return err } var contentLen int64 if fsm.fsize > 0 { contentLen = fsm.fsize } else { lenstr := r.Header.Get("Content-Length") if lenstr == "" { contentLen = -1 } else { tmpLen, err := strconv.ParseInt(lenstr, 10, 64) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to parse content len: %v", err) return &appError{err, msg, http.StatusInternalServerError} } contentLen = tmpLen } } ret, err := checkQuota(repoID, contentLen) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to check quota: %v", err) return &appError{err, msg, http.StatusInternalServerError} } if ret == 1 { msg := "Out of quota.\n" return &appError{nil, msg, seafHTTPResNoQuota} } if err := createRelativePath(repoID, parentDir, relativePath, user); err != nil { return err } if err := postMultiFiles(rsp, r, repoID, newParentDir, user, fsm, replaceExisted, isAjax); err != nil { return err } oper := "web-file-upload" if fsm.tokenType == "upload-link" { oper = "link-file-upload" } sendStatisticMsg(repoID, user, oper, uint64(contentLen)) return nil } func writeBlockDataToTmpFile(r *http.Request, fsm *recvData, formFiles map[string][]*multipart.FileHeader, repoID, parentDir string) error { httpTempDir := filepath.Join(absDataDir, "httptemp") fileHeaders, ok := formFiles["file"] if !ok { err := fmt.Errorf("failed to get file from multipart form") return err } disposition := r.Header.Get("Content-Disposition") if disposition == "" { err := fmt.Errorf("missing content disposition") return err } _, params, err := mime.ParseMediaType(disposition) if err != nil { err := fmt.Errorf("failed to parse Content-Disposition: %v", err) return err } filename, err := url.QueryUnescape(params["filename"]) if err != nil { err := fmt.Errorf("failed to get filename: %v", err) return err } handler := fileHeaders[0] file, err := handler.Open() if err != nil { err := fmt.Errorf("failed to open file for read: %v", err) return err } defer file.Close() var f *os.File //filename := handler.Filename filePath := filepath.Join("/", parentDir, filename) tmpFile, err := repomgr.GetUploadTmpFile(repoID, filePath) if err != nil || tmpFile == "" { tmpDir := filepath.Join(httpTempDir, "cluster-shared") f, err = ioutil.TempFile(tmpDir, filename) if err != nil { return err } repomgr.AddUploadTmpFile(repoID, filePath, f.Name()) tmpFile = f.Name() } else { f, err = os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return err } } if fsm.rend == fsm.fsize-1 { fileName := filepath.Base(filename) fsm.fileNames = append(fsm.fileNames, normalizeUTF8Path(fileName)) fsm.files = append(fsm.files, tmpFile) } f.Seek(fsm.rstart, 0) io.Copy(f, file) f.Close() return nil } func createRelativePath(repoID, parentDir, relativePath, user string) *appError { if relativePath == "" { return nil } err := mkdirWithParents(repoID, parentDir, relativePath, user) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("Failed to create parent directory: %v", err) return &appError{err, msg, http.StatusInternalServerError} } return nil } func mkdirWithParents(repoID, parentDir, newDirPath, user string) error { repo := repomgr.Get(repoID) if repo == nil { err := fmt.Errorf("failed to get repo %s", repoID) return err } headCommit, err := commitmgr.Load(repo.ID, repo.HeadCommitID) if err != nil { err := fmt.Errorf("failed to get head commit for repo %s", repo.ID) return err } relativeDirCan := getCanonPath(newDirPath) subFolders := strings.Split(relativeDirCan, "/") for _, name := range subFolders { if name == "" { continue } if shouldIgnoreFile(name) { err := fmt.Errorf("invalid dir name %s", name) return err } } var rootID string var parentDirCan string if parentDir == "/" || parentDir == "\\" { parentDirCan = "/" } else { parentDirCan = getCanonPath(parentDir) } absPath, dirID, err := checkAndCreateDir(repo, headCommit.RootID, parentDirCan, subFolders) if err != nil { err := fmt.Errorf("failed to check and create dir: %v", err) return err } if absPath == "" { return nil } newRootID := headCommit.RootID mtime := time.Now().Unix() mode := (syscall.S_IFDIR | 0644) dent := fsmgr.NewDirent(dirID, filepath.Base(absPath), uint32(mode), mtime, "", 0) var names []string rootID, _ = doPostMultiFiles(repo, newRootID, filepath.Dir(absPath), []*fsmgr.SeafDirent{dent}, user, false, &names) if rootID == "" { err := fmt.Errorf("failed to put dir") return err } buf := fmt.Sprintf("Added directory \"%s\"", relativeDirCan) _, err = genNewCommit(repo, headCommit, rootID, user, buf, true) if err != nil { err := fmt.Errorf("failed to generate new commit: %v", err) return err } go mergeVirtualRepoPool.AddTask(repo.ID, "") return nil } func checkAndCreateDir(repo *repomgr.Repo, rootID, parentDir string, subFolders []string) (string, string, error) { storeID := repo.StoreID dir, err := fsmgr.GetSeafdirByPath(storeID, rootID, parentDir) if err != nil { err := fmt.Errorf("parent_dir %s doesn't exist in repo %s", parentDir, storeID) return "", "", err } entries := dir.Entries var exists bool var absPath string var dirList []string for i, dirName := range subFolders { for _, de := range entries { if de.Name == dirName { exists = true subDir, err := fsmgr.GetSeafdir(storeID, de.ID) if err != nil { err := fmt.Errorf("failed to get seaf dir: %v", err) return "", "", err } entries = subDir.Entries break } } if !exists { relativePath := filepath.Join(subFolders[:i+1]...) absPath = filepath.Join(parentDir, relativePath) dirList = subFolders[i:] break } exists = false } if dirList != nil { dirList = dirList[1:] } if len(dirList) == 0 { return absPath, "", nil } dirID, err := genDirRecursive(repo, dirList) if err != nil { err := fmt.Errorf("failed to generate dir recursive: %v", err) return "", "", err } return absPath, dirID, nil } func genDirRecursive(repo *repomgr.Repo, toPath []string) (string, error) { if len(toPath) == 1 { uniqueName := toPath[0] mode := (syscall.S_IFDIR | 0644) mtime := time.Now().Unix() dent := fsmgr.NewDirent("", uniqueName, uint32(mode), mtime, "", 0) newdir, err := fsmgr.NewSeafdir(1, []*fsmgr.SeafDirent{dent}) if err != nil { err := fmt.Errorf("failed to new seafdir: %v", err) return "", err } err = fsmgr.SaveSeafdir(repo.StoreID, newdir) if err != nil { err := fmt.Errorf("failed to save seafdir %s/%s", repo.ID, newdir.DirID) return "", err } return newdir.DirID, nil } ret, err := genDirRecursive(repo, toPath[1:]) if err != nil { err := fmt.Errorf("failed to generate dir recursive: %v", err) return "", err } if ret != "" { uniqueName := toPath[0] mode := (syscall.S_IFDIR | 0644) mtime := time.Now().Unix() dent := fsmgr.NewDirent(ret, uniqueName, uint32(mode), mtime, "", 0) newdir, err := fsmgr.NewSeafdir(1, []*fsmgr.SeafDirent{dent}) if err != nil { err := fmt.Errorf("failed to new seafdir: %v", err) return "", err } err = fsmgr.SaveSeafdir(repo.StoreID, newdir) if err != nil { err := fmt.Errorf("failed to save seafdir %s/%s", repo.ID, newdir.DirID) return "", err } ret = newdir.DirID } return ret, nil } func clearTmpFile(fsm *recvData, parentDir string) { if fsm.rstart >= 0 && fsm.rend == fsm.fsize-1 { filePath := filepath.Join("/", parentDir, fsm.fileNames[0]) tmpFile, err := repomgr.GetUploadTmpFile(fsm.repoID, filePath) if err == nil && tmpFile != "" { os.Remove(tmpFile) } repomgr.DelUploadTmpFile(fsm.repoID, filePath) } return } func parseUploadHeaders(r *http.Request) (*recvData, *appError) { tokenLen := 36 parts := strings.Split(r.URL.Path[1:], "/") if len(parts) < 2 { msg := "Invalid URL" return nil, &appError{nil, msg, http.StatusBadRequest} } urlOp := parts[0] if len(parts[1]) < tokenLen { msg := "Invalid URL" return nil, &appError{nil, msg, http.StatusBadRequest} } token := parts[1][:tokenLen] accessInfo, appErr := parseWebaccessInfo(token) if appErr != nil { return nil, appErr } repoID := accessInfo.repoID op := accessInfo.op user := accessInfo.user id := accessInfo.objID status, err := repomgr.GetRepoStatus(repoID) if err != nil { return nil, &appError{err, "", http.StatusInternalServerError} } if status != repomgr.RepoStatusNormal && status != -1 { msg := "Repo status not writable." return nil, &appError{nil, msg, http.StatusBadRequest} } if op == "upload-link" { op = "upload" } if strings.Index(urlOp, op) != 0 { msg := "Operation does not match access token." return nil, &appError{nil, msg, http.StatusForbidden} } fsm := new(recvData) if op != "update" { obj := make(map[string]interface{}) if err := json.Unmarshal([]byte(id), &obj); err != nil { err := fmt.Errorf("failed to decode obj data : %v", err) return nil, &appError{err, "", http.StatusInternalServerError} } parentDir, ok := obj["parent_dir"].(string) if !ok || parentDir == "" { err := fmt.Errorf("no parent_dir in access token") return nil, &appError{err, "", http.StatusInternalServerError} } fsm.parentDir = parentDir } fsm.tokenType = accessInfo.op fsm.repoID = repoID fsm.user = user fsm.rstart = -1 fsm.rend = -1 fsm.fsize = -1 ranges := r.Header.Get("Content-Range") if ranges != "" { parseContentRange(ranges, fsm) } return fsm, nil } func postMultiFiles(rsp http.ResponseWriter, r *http.Request, repoID, parentDir, user string, fsm *recvData, replace bool, isAjax bool) *appError { fileNames := fsm.fileNames files := fsm.files repo := repomgr.Get(repoID) if repo == nil { msg := "Failed to get repo.\n" err := fmt.Errorf("Failed to get repo %s", repoID) return &appError{err, msg, http.StatusInternalServerError} } canonPath := getCanonPath(parentDir) for _, fileName := range fileNames { if shouldIgnoreFile(fileName) { msg := fmt.Sprintf("invalid fileName: %s.\n", fileName) return &appError{nil, msg, http.StatusBadRequest} } } if strings.Index(parentDir, "//") != -1 { msg := "parent_dir contains // sequence.\n" return &appError{nil, msg, http.StatusBadRequest} } var cryptKey *seafileCrypt if repo.IsEncrypted { key, err := parseCryptKey(rsp, repoID, user, repo.EncVersion) if err != nil { return err } cryptKey = key } var ids []string var sizes []int64 if fsm.rstart >= 0 { for _, filePath := range files { id, size, err := indexBlocks(r.Context(), repo.StoreID, repo.Version, filePath, nil, cryptKey) if err != nil { if !errors.Is(err, context.Canceled) { err := fmt.Errorf("failed to index blocks: %v", err) return &appError{err, "", http.StatusInternalServerError} } return &appError{nil, "", http.StatusInternalServerError} } ids = append(ids, id) sizes = append(sizes, size) } } else { for _, handler := range fsm.fileHeaders { id, size, err := indexBlocks(r.Context(), repo.StoreID, repo.Version, "", handler, cryptKey) if err != nil { if !errors.Is(err, context.Canceled) { err := fmt.Errorf("failed to index blocks: %v", err) return &appError{err, "", http.StatusInternalServerError} } return &appError{nil, "", http.StatusInternalServerError} } ids = append(ids, id) sizes = append(sizes, size) } } retStr, err := postFilesAndGenCommit(fileNames, repo.ID, user, canonPath, replace, ids, sizes) if err != nil { err := fmt.Errorf("failed to post files and gen commit: %v", err) return &appError{err, "", http.StatusInternalServerError} } _, ok := r.Form["ret-json"] if ok || isAjax { rsp.Header().Set("Content-Type", "application/json; charset=utf-8") rsp.Write([]byte(retStr)) } else { var array []map[string]interface{} err := json.Unmarshal([]byte(retStr), &array) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to decode data to json: %v", err) return &appError{err, msg, http.StatusInternalServerError} } var ids []string for _, v := range array { id, ok := v["id"].(string) if !ok { msg := "Internal error.\n" err := fmt.Errorf("failed to assert") return &appError{err, msg, http.StatusInternalServerError} } ids = append(ids, id) } newIDs := strings.Join(ids, "\t") rsp.Write([]byte(newIDs)) } return nil } func postFilesAndGenCommit(fileNames []string, repoID string, user, canonPath string, replace bool, ids []string, sizes []int64) (string, error) { repo := repomgr.Get(repoID) if repo == nil { err := fmt.Errorf("failed to get repo %s", repoID) return "", err } headCommit, err := commitmgr.Load(repo.ID, repo.HeadCommitID) if err != nil { err := fmt.Errorf("failed to get head commit for repo %s", repo.ID) return "", err } var names []string var retryCnt int var dents []*fsmgr.SeafDirent for i, name := range fileNames { if i > len(ids)-1 || i > len(sizes)-1 { break } mode := (syscall.S_IFREG | 0644) mtime := time.Now().Unix() dent := fsmgr.NewDirent(ids[i], name, uint32(mode), mtime, "", sizes[i]) dents = append(dents, dent) } retry: rootID, err := doPostMultiFiles(repo, headCommit.RootID, canonPath, dents, user, replace, &names) if err != nil { err := fmt.Errorf("failed to post files to %s in repo %s", canonPath, repo.ID) return "", err } var buf string if len(fileNames) > 1 { buf = fmt.Sprintf("Added \"%s\" and %d more files.", fileNames[0], len(fileNames)-1) } else { buf = fmt.Sprintf("Added \"%s\".", fileNames[0]) } _, err = genNewCommit(repo, headCommit, rootID, user, buf, false) if err != nil { if err != ErrConflict { err := fmt.Errorf("failed to generate new commit: %v", err) return "", err } retryCnt++ /* Sleep random time between 0 and 3 seconds. */ random := rand.Intn(30) + 1 log.Debugf("concurrent upload retry :%d", retryCnt) time.Sleep(time.Duration(random*100) * time.Millisecond) repo = repomgr.Get(repoID) if repo == nil { err := fmt.Errorf("failed to get repo %s", repoID) return "", err } headCommit, err = commitmgr.Load(repo.ID, repo.HeadCommitID) if err != nil { err := fmt.Errorf("failed to get head commit for repo %s", repo.ID) return "", err } goto retry } go mergeVirtualRepoPool.AddTask(repo.ID, "") retJSON, err := formatJSONRet(names, ids, sizes) if err != nil { err := fmt.Errorf("failed to format json data") return "", err } return string(retJSON), nil } func formatJSONRet(nameList, idList []string, sizeList []int64) ([]byte, error) { var array []map[string]interface{} for i := range nameList { if i >= len(idList) || i >= len(sizeList) { break } obj := make(map[string]interface{}) obj["name"] = nameList[i] obj["id"] = idList[i] obj["size"] = sizeList[i] array = append(array, obj) } jsonstr, err := json.Marshal(array) if err != nil { err := fmt.Errorf("failed to convert array to json") return nil, err } return jsonstr, nil } func getCanonPath(p string) string { formatPath := strings.Replace(p, "\\", "/", -1) return filepath.Join(formatPath) } var ErrConflict = fmt.Errorf("Concurent upload conflict") func genNewCommit(repo *repomgr.Repo, base *commitmgr.Commit, newRoot, user, desc string, retryOnConflict bool) (string, error) { var retryCnt int repoID := repo.ID commit := commitmgr.NewCommit(repoID, base.CommitID, newRoot, user, desc) repomgr.RepoToCommit(repo, commit) err := commitmgr.Save(commit) if err != nil { err := fmt.Errorf("failed to add commit: %v", err) return "", err } var commitID string maxRetryCnt := 10 for { retry, err := genCommitNeedRetry(repo, base, commit, newRoot, user, &commitID) if err != nil { return "", err } if !retry { break } if !retryOnConflict { return "", ErrConflict } if retryCnt < maxRetryCnt { /* Sleep random time between 0 and 3 seconds. */ random := rand.Intn(30) + 1 time.Sleep(time.Duration(random*100) * time.Millisecond) repo = repomgr.Get(repoID) if repo == nil { err := fmt.Errorf("repo %s doesn't exist", repoID) return "", err } retryCnt++ } else { err := fmt.Errorf("stop updating repo %s after %d retries", repoID, maxRetryCnt) return "", err } } return commitID, nil } func fastForwardOrMerge(user string, repo *repomgr.Repo, base, newCommit *commitmgr.Commit) error { var retryCnt int for { retry, err := genCommitNeedRetry(repo, base, newCommit, newCommit.RootID, user, nil) if err != nil { return err } if !retry { break } if retryCnt < 3 { random := rand.Intn(10) + 1 time.Sleep(time.Duration(random*100) * time.Millisecond) retryCnt++ } else { err = fmt.Errorf("stop updating repo %s after 3 retries", repo.ID) return err } } return nil } func genCommitNeedRetry(repo *repomgr.Repo, base *commitmgr.Commit, commit *commitmgr.Commit, newRoot, user string, commitID *string) (bool, error) { var secondParentID string repoID := repo.ID var mergeDesc string var mergedCommit *commitmgr.Commit currentHead, err := commitmgr.Load(repo.ID, repo.HeadCommitID) if err != nil { err := fmt.Errorf("failed to get head commit for repo %s", repoID) return false, err } if base.CommitID != currentHead.CommitID { roots := []string{base.RootID, currentHead.RootID, newRoot} opt := new(mergeOptions) opt.remoteRepoID = repoID opt.remoteHead = commit.CommitID err := mergeTrees(repo.StoreID, roots, opt) if err != nil { err := fmt.Errorf("failed to merge") return false, err } if !opt.conflict { mergeDesc = fmt.Sprintf("Auto merge by system") } else { mergeDesc = genMergeDesc(repo, opt.mergedRoot, currentHead.RootID, newRoot) if mergeDesc == "" { mergeDesc = fmt.Sprintf("Auto merge by system") } } secondParentID = commit.CommitID mergedCommit = commitmgr.NewCommit(repoID, currentHead.CommitID, opt.mergedRoot, user, mergeDesc) repomgr.RepoToCommit(repo, mergedCommit) mergedCommit.SecondParentID.SetValid(commit.CommitID) mergedCommit.NewMerge = 1 if opt.conflict { mergedCommit.Conflict = 1 } err = commitmgr.Save(mergedCommit) if err != nil { err := fmt.Errorf("failed to add commit: %v", err) return false, err } } else { mergedCommit = commit } err = updateBranch(repoID, mergedCommit.CommitID, currentHead.CommitID, secondParentID) if err != nil { return true, nil } if commitID != nil { *commitID = mergedCommit.CommitID } return false, nil } func genMergeDesc(repo *repomgr.Repo, mergedRoot, p1Root, p2Root string) string { var results []*diff.DiffEntry err := diff.DiffMergeRoots(repo.StoreID, mergedRoot, p1Root, p2Root, &results, true) if err != nil { return "" } desc := diff.DiffResultsToDesc(results) return desc } func updateBranch(repoID, newCommitID, oldCommitID, secondParentID string) error { var commitID string name := "master" var sqlStr string if strings.EqualFold(dbType, "mysql") { sqlStr = "SELECT commit_id FROM Branch WHERE name = ? AND repo_id = ? FOR UPDATE" } else { sqlStr = "SELECT commit_id FROM Branch WHERE name = ? AND repo_id = ?" } trans, err := seafileDB.Begin() if err != nil { err := fmt.Errorf("failed to start transaction: %v", err) return err } row := trans.QueryRow(sqlStr, name, repoID) if err := row.Scan(&commitID); err != nil { if err != sql.ErrNoRows { trans.Rollback() return err } } if oldCommitID != commitID { trans.Rollback() err := fmt.Errorf("head commit id has changed") return err } sqlStr = "UPDATE Branch SET commit_id = ? WHERE name = ? AND repo_id = ?" _, err = trans.Exec(sqlStr, newCommitID, name, repoID) if err != nil { trans.Rollback() return err } trans.Commit() if secondParentID != "" { if err := onBranchUpdated(repoID, secondParentID, false); err != nil { return err } } if err := onBranchUpdated(repoID, newCommitID, true); err != nil { return err } return nil } func onBranchUpdated(repoID string, commitID string, updateRepoInfo bool) error { if updateRepoInfo { if err := repomgr.UpdateRepoInfo(repoID, commitID); err != nil { return err } } if option.EnableNotification { notifRepoUpdate(repoID, commitID) } isVirtual, err := repomgr.IsVirtualRepo(repoID) if err != nil { return err } if isVirtual { return nil } publishUpdateEvent(repoID, commitID) return nil } type notifEvent struct { Type string `json:"type"` Content *repoUpdateEvent `json:"content"` } type repoUpdateEvent struct { RepoID string `json:"repo_id"` CommitID string `json:"commit_id"` } func notifRepoUpdate(repoID string, commitID string) error { content := new(repoUpdateEvent) content.RepoID = repoID content.CommitID = commitID event := new(notifEvent) event.Type = "repo-update" event.Content = content msg, err := json.Marshal(event) if err != nil { log.Printf("failed to encode repo update event: %v", err) return err } url := fmt.Sprintf("http://%s/events", option.NotificationURL) token, err := genJWTToken(repoID, "") if err != nil { log.Printf("failed to generate jwt token: %v", err) return err } header := map[string][]string{ "Seafile-Repo-Token": {token}, "Content-Type": {"application/json"}, } _, _, err = httpCommon("POST", url, header, bytes.NewReader(msg)) if err != nil { log.Printf("failed to send repo update event: %v", err) return err } return nil } func httpCommon(method, url string, header map[string][]string, reader io.Reader) (int, []byte, error) { req, err := http.NewRequest(method, url, reader) if err != nil { return -1, nil, err } req.Header = header rsp, err := http.DefaultClient.Do(req) if err != nil { return -1, nil, err } defer rsp.Body.Close() if rsp.StatusCode != http.StatusOK { return rsp.StatusCode, nil, fmt.Errorf("bad response %d for %s", rsp.StatusCode, url) } body, err := io.ReadAll(rsp.Body) if err != nil { return rsp.StatusCode, nil, err } return rsp.StatusCode, body, nil } func doPostMultiFiles(repo *repomgr.Repo, rootID, parentDir string, dents []*fsmgr.SeafDirent, user string, replace bool, names *[]string) (string, error) { if parentDir[0] == '/' { parentDir = parentDir[1:] } id, err := postMultiFilesRecursive(repo, rootID, parentDir, user, dents, replace, names) if err != nil { err := fmt.Errorf("failed to post multi files: %v", err) return "", err } return id, nil } func postMultiFilesRecursive(repo *repomgr.Repo, dirID, toPath, user string, dents []*fsmgr.SeafDirent, replace bool, names *[]string) (string, error) { olddir, err := fsmgr.GetSeafdir(repo.StoreID, dirID) if err != nil { err := fmt.Errorf("failed to get dir") return "", err } var ret string if toPath == "" { err := addNewEntries(repo, user, &olddir.Entries, dents, replace, names) if err != nil { err := fmt.Errorf("failed to add new entries: %v", err) return "", err } newdir, err := fsmgr.NewSeafdir(1, olddir.Entries) if err != nil { err := fmt.Errorf("failed to new seafdir: %v", err) return "", err } err = fsmgr.SaveSeafdir(repo.StoreID, newdir) if err != nil { err := fmt.Errorf("failed to save seafdir %s/%s", repo.ID, newdir.DirID) return "", err } return newdir.DirID, nil } var remain string firstName := toPath if slash := strings.Index(toPath, "/"); slash >= 0 { remain = toPath[slash+1:] firstName = toPath[:slash] } entries := olddir.Entries for i, dent := range entries { if dent.Name != firstName { continue } id, err := postMultiFilesRecursive(repo, dent.ID, remain, user, dents, replace, names) if err != nil { err := fmt.Errorf("failed to post dirent %s: %v", dent.Name, err) return "", err } ret = id if id != "" { entries[i].ID = id entries[i].Mtime = time.Now().Unix() } break } if ret != "" { newdir, err := fsmgr.NewSeafdir(1, entries) if err != nil { err := fmt.Errorf("failed to new seafdir: %v", err) return "", err } err = fsmgr.SaveSeafdir(repo.StoreID, newdir) if err != nil { err := fmt.Errorf("failed to save seafdir %s/%s", repo.ID, newdir.DirID) return "", err } ret = newdir.DirID } else { // The ret will be an empty string when failed to find parent dir, an error should be returned in such case. err := fmt.Errorf("failed to find parent dir for %s", toPath) return "", err } return ret, nil } func addNewEntries(repo *repomgr.Repo, user string, oldDents *[]*fsmgr.SeafDirent, newDents []*fsmgr.SeafDirent, replaceExisted bool, names *[]string) error { for _, dent := range newDents { var replace bool var uniqueName string if replaceExisted { for i, entry := range *oldDents { if entry.Name == dent.Name { replace = true *oldDents = append((*oldDents)[:i], (*oldDents)[i+1:]...) break } } } if replace { uniqueName = dent.Name } else { uniqueName = genUniqueName(dent.Name, *oldDents) } if uniqueName != "" { newDent := fsmgr.NewDirent(dent.ID, uniqueName, dent.Mode, dent.Mtime, user, dent.Size) *oldDents = append(*oldDents, newDent) *names = append(*names, uniqueName) } else { err := fmt.Errorf("failed to generate unique name for %s", dent.Name) return err } } sort.Sort(Dirents(*oldDents)) return nil } func genUniqueName(fileName string, entries []*fsmgr.SeafDirent) string { var uniqueName string var name string i := 1 dot := strings.Index(fileName, ".") if dot < 0 { name = fileName } else { name = fileName[:dot] } uniqueName = fileName for nameExists(entries, uniqueName) && i <= duplicateNamesCount { if dot < 0 { uniqueName = fmt.Sprintf("%s (%d)", name, i) } else { uniqueName = fmt.Sprintf("%s (%d).%s", name, i, fileName[dot+1:]) } i++ } if i <= duplicateNamesCount { return uniqueName } return "" } func nameExists(entries []*fsmgr.SeafDirent, fileName string) bool { for _, entry := range entries { if entry.Name == fileName { return true } } return false } func shouldIgnoreFile(fileName string) bool { if !utf8.ValidString(fileName) { log.Printf("file name %s contains non-UTF8 characters, skip", fileName) return true } if len(fileName) >= 256 { return true } if strings.Index(fileName, "/") != -1 { return true } return false } func indexBlocks(ctx context.Context, repoID string, version int, filePath string, handler *multipart.FileHeader, cryptKey *seafileCrypt) (string, int64, error) { var size int64 if handler != nil { size = handler.Size } else { f, err := os.Open(filePath) if err != nil { err := fmt.Errorf("failed to open file: %s: %v", filePath, err) return "", -1, err } defer f.Close() fileInfo, err := f.Stat() if err != nil { err := fmt.Errorf("failed to stat file %s: %v", filePath, err) return "", -1, err } size = fileInfo.Size() } if size == 0 { return fsmgr.EmptySha1, 0, nil } chunkJobs := make(chan chunkingData, 10) results := make(chan chunkingResult, 10) go createChunkPool(ctx, int(option.MaxIndexingThreads), chunkJobs, results) var blkSize int64 var offset int64 jobNum := (uint64(size) + option.FixedBlockSize - 1) / option.FixedBlockSize blkIDs := make([]string, jobNum) left := size for { if uint64(left) >= option.FixedBlockSize { blkSize = int64(option.FixedBlockSize) } else { blkSize = left } if left > 0 { job := chunkingData{repoID, filePath, handler, offset, cryptKey} select { case chunkJobs <- job: left -= blkSize offset += blkSize case result := <-results: if result.err != nil { close(chunkJobs) go RecoverWrapper(func() { for result := range results { _ = result } }) return "", -1, result.err } blkIDs[result.idx] = result.blkID } } else { close(chunkJobs) for result := range results { if result.err != nil { go RecoverWrapper(func() { for result := range results { _ = result } }) return "", -1, result.err } blkIDs[result.idx] = result.blkID } break } } fileID, err := writeSeafile(repoID, version, size, blkIDs) if err != nil { err := fmt.Errorf("failed to write seafile: %v", err) return "", -1, err } return fileID, size, nil } func writeSeafile(repoID string, version int, fileSize int64, blkIDs []string) (string, error) { seafile, err := fsmgr.NewSeafile(version, fileSize, blkIDs) if err != nil { err := fmt.Errorf("failed to new seafile: %v", err) return "", err } err = fsmgr.SaveSeafile(repoID, seafile) if err != nil { err := fmt.Errorf("failed to save seafile %s/%s", repoID, seafile.FileID) return "", err } return seafile.FileID, nil } type chunkingData struct { repoID string filePath string handler *multipart.FileHeader offset int64 cryptKey *seafileCrypt } type chunkingResult struct { idx int64 blkID string err error } func createChunkPool(ctx context.Context, n int, chunkJobs chan chunkingData, res chan chunkingResult) { defer func() { if err := recover(); err != nil { log.Printf("panic: %v\n%s", err, debug.Stack()) } }() var wg sync.WaitGroup for i := 0; i < n; i++ { wg.Add(1) go chunkingWorker(ctx, &wg, chunkJobs, res) } wg.Wait() close(res) } func chunkingWorker(ctx context.Context, wg *sync.WaitGroup, chunkJobs chan chunkingData, res chan chunkingResult) { defer func() { if err := recover(); err != nil { log.Printf("panic: %v\n%s", err, debug.Stack()) } }() for job := range chunkJobs { select { case <-ctx.Done(): err := context.Canceled result := chunkingResult{-1, "", err} res <- result wg.Done() return default: } job := job blkID, err := chunkFile(job) idx := job.offset / int64(option.FixedBlockSize) result := chunkingResult{idx, blkID, err} res <- result } wg.Done() } func chunkFile(job chunkingData) (string, error) { repoID := job.repoID offset := job.offset filePath := job.filePath handler := job.handler blkSize := option.FixedBlockSize cryptKey := job.cryptKey var file multipart.File if handler != nil { f, err := handler.Open() if err != nil { err := fmt.Errorf("failed to open file for read: %v", err) return "", err } defer f.Close() file = f } else { f, err := os.Open(filePath) if err != nil { err := fmt.Errorf("failed to open file for read: %v", err) return "", err } defer f.Close() file = f } _, err := file.Seek(offset, os.SEEK_SET) if err != nil { err := fmt.Errorf("failed to seek file: %v", err) return "", err } buf := make([]byte, blkSize) n, err := file.Read(buf) if err != nil { err := fmt.Errorf("failed to seek file: %v", err) return "", err } buf = buf[:n] blkID, err := writeChunk(repoID, buf, int64(n), cryptKey) if err != nil { err := fmt.Errorf("failed to write chunk: %v", err) return "", err } return blkID, nil } func writeChunk(repoID string, input []byte, blkSize int64, cryptKey *seafileCrypt) (string, error) { var blkID string if cryptKey != nil && blkSize > 0 { encoded, err := cryptKey.encrypt(input) if err != nil { err := fmt.Errorf("failed to encrypt block: %v", err) return "", err } checkSum := sha1.Sum(encoded) blkID = hex.EncodeToString(checkSum[:]) if blockmgr.Exists(repoID, blkID) { return blkID, nil } reader := bytes.NewReader(encoded) err = blockmgr.Write(repoID, blkID, reader) if err != nil { err := fmt.Errorf("failed to write block: %v", err) return "", err } } else { checkSum := sha1.Sum(input) blkID = hex.EncodeToString(checkSum[:]) if blockmgr.Exists(repoID, blkID) { return blkID, nil } reader := bytes.NewReader(input) err := blockmgr.Write(repoID, blkID, reader) if err != nil { err := fmt.Errorf("failed to write block: %v", err) return "", err } } return blkID, nil } func checkTmpFileList(fsm *recvData) *appError { var totalSize int64 if fsm.rstart >= 0 { for _, tmpFile := range fsm.files { fileInfo, err := os.Stat(tmpFile) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("[upload] Failed to stat temp file %s", tmpFile) return &appError{err, msg, http.StatusInternalServerError} } totalSize += fileInfo.Size() } } else { for _, handler := range fsm.fileHeaders { totalSize += handler.Size } } if option.MaxUploadSize > 0 && uint64(totalSize) > option.MaxUploadSize { msg := "File size is too large.\n" return &appError{nil, msg, seafHTTPResTooLarge} } return nil } func checkParentDir(repoID string, parentDir string) *appError { repo := repomgr.Get(repoID) if repo == nil { msg := "Failed to get repo.\n" err := fmt.Errorf("Failed to get repo %s", repoID) return &appError{err, msg, http.StatusInternalServerError} } commit, err := commitmgr.Load(repoID, repo.HeadCommitID) if err != nil { msg := "Failed to get head commit.\n" err := fmt.Errorf("Failed to get head commit for repo %s", repoID) return &appError{err, msg, http.StatusInternalServerError} } canonPath := getCanonPath(parentDir) _, err = fsmgr.GetSeafdirByPath(repo.StoreID, commit.RootID, canonPath) if err != nil { msg := "Parent dir doesn't exist.\n" return &appError{nil, msg, http.StatusBadRequest} } return nil } func isParentMatched(uploadDir, parentDir string) bool { uploadCanon := filepath.Join("/", uploadDir) parentCanon := filepath.Join("/", parentDir) if uploadCanon != parentCanon { return false } return true } func parseContentRange(ranges string, fsm *recvData) bool { start := strings.Index(ranges, "bytes") end := strings.Index(ranges, "-") slash := strings.Index(ranges, "/") if start < 0 || end < 0 || slash < 0 { return false } startStr := strings.TrimLeft(ranges[start+len("bytes"):end], " ") firstByte, err := strconv.ParseInt(startStr, 10, 64) if err != nil { return false } lastByte, err := strconv.ParseInt(ranges[end+1:slash], 10, 64) if err != nil { return false } fileSize, err := strconv.ParseInt(ranges[slash+1:], 10, 64) if err != nil { return false } if firstByte > lastByte || lastByte >= fileSize { return false } fsm.rstart = firstByte fsm.rend = lastByte fsm.fsize = fileSize return true } type webaccessInfo struct { repoID string objID string op string user string } func parseWebaccessInfo(token string) (*webaccessInfo, *appError) { webaccess, err := rpcclient.Call("seafile_web_query_access_token", token) if err != nil { err := fmt.Errorf("failed to get web access token: %v", err) return nil, &appError{err, "", http.StatusInternalServerError} } if webaccess == nil { msg := "Access token not found" return nil, &appError{err, msg, http.StatusForbidden} } webaccessMap, ok := webaccess.(map[string]interface{}) if !ok { return nil, &appError{nil, "", http.StatusInternalServerError} } accessInfo := new(webaccessInfo) repoID, ok := webaccessMap["repo-id"].(string) if !ok { return nil, &appError{nil, "", http.StatusInternalServerError} } accessInfo.repoID = repoID id, ok := webaccessMap["obj-id"].(string) if !ok { return nil, &appError{nil, "", http.StatusInternalServerError} } accessInfo.objID = id op, ok := webaccessMap["op"].(string) if !ok { return nil, &appError{nil, "", http.StatusInternalServerError} } accessInfo.op = op user, ok := webaccessMap["username"].(string) if !ok { return nil, &appError{nil, "", http.StatusInternalServerError} } accessInfo.user = user return accessInfo, nil } func updateDir(repoID, dirPath, newDirID, user, headID string) (string, error) { repo := repomgr.Get(repoID) if repo == nil { err := fmt.Errorf("failed to get repo %.10s", repoID) return "", err } var base string if headID == "" { base = repo.HeadCommitID } else { base = headID } headCommit, err := commitmgr.Load(repo.ID, base) if err != nil { err := fmt.Errorf("failed to get head commit for repo %s", repo.ID) return "", err } if dirPath == "/" { commitDesc := genCommitDesc(repo, newDirID, headCommit.RootID) if commitDesc == "" { commitDesc = fmt.Sprintf("Auto merge by system") } newCommitID, err := genNewCommit(repo, headCommit, newDirID, user, commitDesc, true) if err != nil { err := fmt.Errorf("failed to generate new commit: %v", err) return "", err } return newCommitID, nil } parent := filepath.Dir(dirPath) canonPath := getCanonPath(parent) dirName := filepath.Base(dirPath) dir, err := fsmgr.GetSeafdirByPath(repo.StoreID, headCommit.RootID, canonPath) if err != nil { err := fmt.Errorf("dir %s doesn't exist in repo %s", canonPath, repo.StoreID) return "", err } var exists bool for _, de := range dir.Entries { if de.Name == dirName { exists = true } } if !exists { err := fmt.Errorf("directory %s doesn't exist in repo %s", dirName, repo.StoreID) return "", err } newDent := fsmgr.NewDirent(newDirID, dirName, (syscall.S_IFDIR | 0644), time.Now().Unix(), "", 0) rootID, err := doPutFile(repo, headCommit.RootID, canonPath, newDent) if err != nil || rootID == "" { err := fmt.Errorf("failed to put file") return "", err } commitDesc := genCommitDesc(repo, rootID, headCommit.RootID) if commitDesc == "" { commitDesc = fmt.Sprintf("Auto merge by system") } newCommitID, err := genNewCommit(repo, headCommit, rootID, user, commitDesc, true) if err != nil { err := fmt.Errorf("failed to generate new commit: %v", err) return "", err } go updateSizePool.AddTask(repoID) return newCommitID, nil } func genCommitDesc(repo *repomgr.Repo, root, parentRoot string) string { var results []*diff.DiffEntry err := diff.DiffCommitRoots(repo.StoreID, parentRoot, root, &results, true) if err != nil { return "" } desc := diff.DiffResultsToDesc(results) return desc } func doPutFile(repo *repomgr.Repo, rootID, parentDir string, dent *fsmgr.SeafDirent) (string, error) { if strings.Index(parentDir, "/") == 0 { parentDir = parentDir[1:] } return putFileRecursive(repo, rootID, parentDir, dent) } func putFileRecursive(repo *repomgr.Repo, dirID, toPath string, newDent *fsmgr.SeafDirent) (string, error) { olddir, err := fsmgr.GetSeafdir(repo.StoreID, dirID) if err != nil { err := fmt.Errorf("failed to get dir") return "", err } entries := olddir.Entries var ret string if toPath == "" { var newEntries []*fsmgr.SeafDirent for _, dent := range entries { if dent.Name == newDent.Name { newEntries = append(newEntries, newDent) } else { newEntries = append(newEntries, dent) } } newdir, err := fsmgr.NewSeafdir(1, newEntries) if err != nil { err := fmt.Errorf("failed to new seafdir: %v", err) return "", err } err = fsmgr.SaveSeafdir(repo.StoreID, newdir) if err != nil { err := fmt.Errorf("failed to save seafdir %s/%s", repo.ID, newdir.DirID) return "", err } return newdir.DirID, nil } var remain string firstName := toPath if slash := strings.Index(toPath, "/"); slash >= 0 { remain = toPath[slash+1:] firstName = toPath[:slash] } for _, dent := range entries { if dent.Name != firstName { continue } id, err := putFileRecursive(repo, dent.ID, remain, newDent) if err != nil { err := fmt.Errorf("failed to put dirent %s: %v", dent.Name, err) return "", err } if id != "" { dent.ID = id dent.Mtime = time.Now().Unix() } ret = id break } if ret != "" { newdir, err := fsmgr.NewSeafdir(1, entries) if err != nil { err := fmt.Errorf("failed to new seafdir: %v", err) return "", err } err = fsmgr.SaveSeafdir(repo.StoreID, newdir) if err != nil { err := fmt.Errorf("failed to save seafdir %s/%s", repo.ID, newdir.DirID) return "", err } ret = newdir.DirID } else { err := fmt.Errorf("failed to find parent dir for %s", toPath) return "", err } return ret, nil } func updateAPICB(rsp http.ResponseWriter, r *http.Request) *appError { if r.Method == "OPTIONS" { setAccessControl(rsp) rsp.WriteHeader(http.StatusOK) return nil } fsm, err := parseUploadHeaders(r) if err != nil { return err } if err := doUpdate(rsp, r, fsm, false); err != nil { formatJSONError(rsp, err) return err } return nil } func updateAjaxCB(rsp http.ResponseWriter, r *http.Request) *appError { if r.Method == "OPTIONS" { setAccessControl(rsp) rsp.WriteHeader(http.StatusOK) return nil } fsm, err := parseUploadHeaders(r) if err != nil { return err } if err := doUpdate(rsp, r, fsm, true); err != nil { formatJSONError(rsp, err) return err } return nil } func doUpdate(rsp http.ResponseWriter, r *http.Request, fsm *recvData, isAjax bool) *appError { setAccessControl(rsp) if err := r.ParseMultipartForm(1 << 20); err != nil { return &appError{nil, "", http.StatusBadRequest} } defer r.MultipartForm.RemoveAll() repoID := fsm.repoID user := fsm.user targetFile := normalizeUTF8Path(r.FormValue("target_file")) if targetFile == "" { msg := "No target_file given.\n" return &appError{nil, msg, http.StatusBadRequest} } parentDir := filepath.Dir(targetFile) fileName := filepath.Base(targetFile) defer clearTmpFile(fsm, parentDir) if fsm.rstart >= 0 { if parentDir[0] != '/' { msg := "Invalid parent dir" return &appError{nil, msg, http.StatusBadRequest} } formFiles := r.MultipartForm.File files, ok := formFiles["file"] if !ok { msg := "No file in multipart form.\n" return &appError{nil, msg, http.StatusBadRequest} } if len(files) > 1 { msg := "More files in one request" return &appError{nil, msg, http.StatusBadRequest} } err := writeBlockDataToTmpFile(r, fsm, formFiles, repoID, parentDir) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to write block data to tmp file: %v", err) return &appError{err, msg, http.StatusInternalServerError} } if fsm.rend != fsm.fsize-1 { rsp.Header().Set("Content-Type", "application/json; charset=utf-8") success := "{\"success\": true}" _, err := rsp.Write([]byte(success)) if err != nil { log.Printf("failed to write data to response.\n") } return nil } } else { formFiles := r.MultipartForm.File fileHeaders, ok := formFiles["file"] if !ok { msg := "No file in multipart form.\n" return &appError{nil, msg, http.StatusBadRequest} } if len(fileHeaders) > 1 { msg := "More files in one request" return &appError{nil, msg, http.StatusBadRequest} } for _, handler := range fileHeaders { fileName := filepath.Base(handler.Filename) fsm.fileNames = append(fsm.fileNames, fileName) fsm.fileHeaders = append(fsm.fileHeaders, handler) } } if fsm.fileNames == nil { msg := "No file.\n" return &appError{nil, msg, http.StatusBadRequest} } if err := checkParentDir(repoID, parentDir); err != nil { return err } if err := checkTmpFileList(fsm); err != nil { return err } var contentLen int64 if fsm.fsize > 0 { contentLen = fsm.fsize } else { lenstr := r.Header.Get("Content-Length") if lenstr == "" { contentLen = -1 } else { tmpLen, err := strconv.ParseInt(lenstr, 10, 64) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to parse content len: %v", err) return &appError{err, msg, http.StatusInternalServerError} } contentLen = tmpLen } } ret, err := checkQuota(repoID, contentLen) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to check quota: %v", err) return &appError{err, msg, http.StatusInternalServerError} } if ret == 1 { msg := "Out of quota.\n" return &appError{nil, msg, seafHTTPResNoQuota} } headIDs, ok := r.Form["head"] var headID string if ok { headID = headIDs[0] } if err := putFile(rsp, r, repoID, parentDir, user, fileName, fsm, headID, isAjax); err != nil { return err } oper := "web-file-upload" sendStatisticMsg(repoID, user, oper, uint64(contentLen)) return nil } func putFile(rsp http.ResponseWriter, r *http.Request, repoID, parentDir, user, fileName string, fsm *recvData, headID string, isAjax bool) *appError { files := fsm.files repo := repomgr.Get(repoID) if repo == nil { msg := "Failed to get repo.\n" err := fmt.Errorf("Failed to get repo %s", repoID) return &appError{err, msg, http.StatusInternalServerError} } var base string if headID != "" { base = headID } else { base = repo.HeadCommitID } headCommit, err := commitmgr.Load(repo.ID, base) if err != nil { msg := "Failed to get head commit.\n" err := fmt.Errorf("failed to get head commit for repo %s", repo.ID) return &appError{err, msg, http.StatusInternalServerError} } canonPath := getCanonPath(parentDir) if shouldIgnoreFile(fileName) { msg := fmt.Sprintf("invalid fileName: %s.\n", fileName) return &appError{nil, msg, http.StatusBadRequest} } if strings.Index(parentDir, "//") != -1 { msg := "parent_dir contains // sequence.\n" return &appError{nil, msg, http.StatusBadRequest} } exist, _ := checkFileExists(repo.StoreID, headCommit.RootID, canonPath, fileName) if !exist { msg := "File does not exist.\n" return &appError{nil, msg, seafHTTPResNotExists} } var cryptKey *seafileCrypt if repo.IsEncrypted { key, err := parseCryptKey(rsp, repoID, user, repo.EncVersion) if err != nil { return err } cryptKey = key } var fileID string var size int64 if fsm.rstart >= 0 { filePath := files[0] id, fileSize, err := indexBlocks(r.Context(), repo.StoreID, repo.Version, filePath, nil, cryptKey) if err != nil { if !errors.Is(err, context.Canceled) { err := fmt.Errorf("failed to index blocks: %w", err) return &appError{err, "", http.StatusInternalServerError} } return &appError{nil, "", http.StatusInternalServerError} } fileID = id size = fileSize } else { handler := fsm.fileHeaders[0] id, fileSize, err := indexBlocks(r.Context(), repo.StoreID, repo.Version, "", handler, cryptKey) if err != nil { if !errors.Is(err, context.Canceled) { err := fmt.Errorf("failed to index blocks: %w", err) return &appError{err, "", http.StatusInternalServerError} } return &appError{nil, "", http.StatusInternalServerError} } fileID = id size = fileSize } fullPath := filepath.Join(parentDir, fileName) oldFileID, _, _ := fsmgr.GetObjIDByPath(repo.StoreID, headCommit.RootID, fullPath) if fileID == oldFileID { if isAjax { retJSON, err := formatUpdateJSONRet(fileName, fileID, size) if err != nil { err := fmt.Errorf("failed to format json data") return &appError{err, "", http.StatusInternalServerError} } rsp.Write(retJSON) } else { rsp.Write([]byte(fileID)) } return nil } mtime := time.Now().Unix() mode := (syscall.S_IFREG | 0644) newDent := fsmgr.NewDirent(fileID, fileName, uint32(mode), mtime, user, size) var names []string rootID, err := doPostMultiFiles(repo, headCommit.RootID, canonPath, []*fsmgr.SeafDirent{newDent}, user, true, &names) if err != nil { err := fmt.Errorf("failed to put file %s to %s in repo %s: %v", fileName, canonPath, repo.ID, err) return &appError{err, "", http.StatusInternalServerError} } desc := fmt.Sprintf("Modified \"%s\"", fileName) _, err = genNewCommit(repo, headCommit, rootID, user, desc, true) if err != nil { err := fmt.Errorf("failed to generate new commit: %v", err) return &appError{err, "", http.StatusInternalServerError} } if isAjax { retJSON, err := formatUpdateJSONRet(fileName, fileID, size) if err != nil { err := fmt.Errorf("failed to format json data") return &appError{err, "", http.StatusInternalServerError} } rsp.Header().Set("Content-Type", "application/json; charset=utf-8") rsp.Write(retJSON) } else { rsp.Write([]byte(fileID)) } go mergeVirtualRepoPool.AddTask(repo.ID) return nil } func formatUpdateJSONRet(fileName, fileID string, size int64) ([]byte, error) { var array []map[string]interface{} obj := make(map[string]interface{}) obj["name"] = fileName obj["id"] = fileID obj["size"] = size array = append(array, obj) jsonstr, err := json.Marshal(array) if err != nil { err := fmt.Errorf("failed to convert array to json") return nil, err } return jsonstr, nil } func checkFileExists(storeID, rootID, parentDir, fileName string) (bool, error) { dir, err := fsmgr.GetSeafdirByPath(storeID, rootID, parentDir) if err != nil { err := fmt.Errorf("parent_dir %s doesn't exist in repo %s: %v", parentDir, storeID, err) return false, err } var ret bool entries := dir.Entries for _, de := range entries { if de.Name == fileName { ret = true break } } return ret, nil } func uploadBlksAPICB(rsp http.ResponseWriter, r *http.Request) *appError { fsm, err := parseUploadHeaders(r) if err != nil { return err } if err := doUploadBlks(rsp, r, fsm); err != nil { formatJSONError(rsp, err) return err } return nil } func doUploadBlks(rsp http.ResponseWriter, r *http.Request, fsm *recvData) *appError { if err := r.ParseMultipartForm(1 << 20); err != nil { return &appError{nil, "", http.StatusBadRequest} } defer r.MultipartForm.RemoveAll() repoID := fsm.repoID user := fsm.user replaceStr := r.FormValue("replace") var replaceExisted bool if replaceStr != "" { replace, err := strconv.ParseInt(replaceStr, 10, 64) if err != nil || (replace != 0 && replace != 1) { msg := "Invalid argument replace.\n" return &appError{nil, msg, http.StatusBadRequest} } if replace == 1 { replaceExisted = true } } parentDir := normalizeUTF8Path(r.FormValue("parent_dir")) if parentDir == "" { msg := "No parent_dir given.\n" return &appError{nil, msg, http.StatusBadRequest} } fileName := normalizeUTF8Path(r.FormValue("file_name")) if fileName == "" { msg := "No file_name given.\n" return &appError{nil, msg, http.StatusBadRequest} } fileSizeStr := r.FormValue("file_size") var fileSize int64 = -1 if fileSizeStr != "" { size, err := strconv.ParseInt(fileSizeStr, 10, 64) if err != nil { msg := "Invalid argument file_size.\n" return &appError{nil, msg, http.StatusBadRequest} } fileSize = size } if fileSize < 0 { msg := "Invalid file size.\n" return &appError{nil, msg, http.StatusBadRequest} } commitOnlyStr, ok := r.Form["commitonly"] if !ok || len(commitOnlyStr) == 0 { msg := "Only commit supported.\n" return &appError{nil, msg, http.StatusBadRequest} } if err := checkParentDir(repoID, parentDir); err != nil { return err } blockIDsJSON := r.FormValue("blockids") if blockIDsJSON == "" { msg := "No blockids given.\n" return &appError{nil, msg, http.StatusBadRequest} } fileID, appErr := commitFileBlocks(repoID, parentDir, fileName, blockIDsJSON, user, fileSize, replaceExisted) if appErr != nil { return appErr } _, ok = r.Form["ret-json"] if ok { obj := make(map[string]interface{}) obj["id"] = fileID jsonstr, err := json.Marshal(obj) if err != nil { err := fmt.Errorf("failed to convert array to json: %v", err) return &appError{err, "", http.StatusInternalServerError} } rsp.Header().Set("Content-Type", "application/json; charset=utf-8") rsp.Write([]byte(jsonstr)) } else { rsp.Header().Set("Content-Type", "application/json; charset=utf-8") rsp.Write([]byte("\"")) rsp.Write([]byte(fileID)) rsp.Write([]byte("\"")) } return nil } func commitFileBlocks(repoID, parentDir, fileName, blockIDsJSON, user string, fileSize int64, replace bool) (string, *appError) { repo := repomgr.Get(repoID) if repo == nil { msg := "Failed to get repo.\n" err := fmt.Errorf("Failed to get repo %s", repoID) return "", &appError{err, msg, http.StatusInternalServerError} } headCommit, err := commitmgr.Load(repo.ID, repo.HeadCommitID) if err != nil { msg := "Failed to get head commit.\n" err := fmt.Errorf("failed to get head commit for repo %s", repo.ID) return "", &appError{err, msg, http.StatusInternalServerError} } canonPath := getCanonPath(parentDir) if shouldIgnoreFile(fileName) { msg := fmt.Sprintf("invalid fileName: %s.\n", fileName) return "", &appError{nil, msg, http.StatusBadRequest} } if strings.Index(parentDir, "//") != -1 { msg := "parent_dir contains // sequence.\n" return "", &appError{nil, msg, http.StatusBadRequest} } var blkIDs []string err = json.Unmarshal([]byte(blockIDsJSON), &blkIDs) if err != nil { err := fmt.Errorf("failed to decode data to json: %v", err) return "", &appError{err, "", http.StatusInternalServerError} } appErr := checkQuotaBeforeCommitBlocks(repo.StoreID, blkIDs) if appErr != nil { return "", appErr } fileID, appErr := indexExistedFileBlocks(repoID, repo.Version, blkIDs, fileSize) if appErr != nil { return "", appErr } mtime := time.Now().Unix() mode := (syscall.S_IFREG | 0644) newDent := fsmgr.NewDirent(fileID, fileName, uint32(mode), mtime, user, fileSize) var names []string rootID, err := doPostMultiFiles(repo, headCommit.RootID, canonPath, []*fsmgr.SeafDirent{newDent}, user, replace, &names) if err != nil { err := fmt.Errorf("failed to post file %s to %s in repo %s: %v", fileName, canonPath, repo.ID, err) return "", &appError{err, "", http.StatusInternalServerError} } desc := fmt.Sprintf("Added \"%s\"", fileName) _, err = genNewCommit(repo, headCommit, rootID, user, desc, true) if err != nil { err := fmt.Errorf("failed to generate new commit: %v", err) return "", &appError{err, "", http.StatusInternalServerError} } return fileID, nil } func checkQuotaBeforeCommitBlocks(storeID string, blockIDs []string) *appError { var totalSize int64 for _, blkID := range blockIDs { size, err := blockmgr.Stat(storeID, blkID) if err != nil { err := fmt.Errorf("failed to stat block %s in store %s: %v", blkID, storeID, err) return &appError{err, "", http.StatusInternalServerError} } totalSize += size } ret, err := checkQuota(storeID, totalSize) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to check quota: %v", err) return &appError{err, msg, http.StatusInternalServerError} } if ret == 1 { msg := "Out of quota.\n" return &appError{nil, msg, seafHTTPResNoQuota} } return nil } func indexExistedFileBlocks(repoID string, version int, blkIDs []string, fileSize int64) (string, *appError) { if len(blkIDs) == 0 { return fsmgr.EmptySha1, nil } for _, blkID := range blkIDs { if !blockmgr.Exists(repoID, blkID) { err := fmt.Errorf("failed to check block: %s", blkID) return "", &appError{err, "", seafHTTPResBlockMissing} } } fileID, err := writeSeafile(repoID, version, fileSize, blkIDs) if err != nil { err := fmt.Errorf("failed to write seafile: %v", err) return "", &appError{err, "", http.StatusInternalServerError} } return fileID, nil } func uploadRawBlksAPICB(rsp http.ResponseWriter, r *http.Request) *appError { fsm, err := parseUploadHeaders(r) if err != nil { return err } if err := doUploadRawBlks(rsp, r, fsm); err != nil { formatJSONError(rsp, err) return err } return nil } func doUploadRawBlks(rsp http.ResponseWriter, r *http.Request, fsm *recvData) *appError { if err := r.ParseMultipartForm(1 << 20); err != nil { return &appError{nil, "", http.StatusBadRequest} } defer r.MultipartForm.RemoveAll() repoID := fsm.repoID user := fsm.user formFiles := r.MultipartForm.File fileHeaders, ok := formFiles["file"] if !ok { msg := "No file in multipart form.\n" return &appError{nil, msg, http.StatusBadRequest} } for _, handler := range fileHeaders { fileName := filepath.Base(handler.Filename) fsm.fileNames = append(fsm.fileNames, fileName) fsm.fileHeaders = append(fsm.fileHeaders, handler) } if fsm.fileNames == nil { msg := "No file.\n" return &appError{nil, msg, http.StatusBadRequest} } if err := checkTmpFileList(fsm); err != nil { return err } if err := postBlocks(repoID, user, fsm); err != nil { return err } var contentLen int64 lenstr := r.Header.Get("Content-Length") if lenstr != "" { conLen, err := strconv.ParseInt(lenstr, 10, 64) if err != nil { msg := "Internal error.\n" err := fmt.Errorf("failed to parse content len: %v", err) return &appError{err, msg, http.StatusInternalServerError} } contentLen = conLen } oper := "web-file-upload" sendStatisticMsg(repoID, user, oper, uint64(contentLen)) rsp.Header().Set("Content-Type", "application/json; charset=utf-8") rsp.Write([]byte("\"OK\"")) return nil } func postBlocks(repoID, user string, fsm *recvData) *appError { blockIDs := fsm.fileNames fileHeaders := fsm.fileHeaders repo := repomgr.Get(repoID) if repo == nil { msg := "Failed to get repo.\n" err := fmt.Errorf("Failed to get repo %s", repoID) return &appError{err, msg, http.StatusInternalServerError} } if err := indexRawBlocks(repo.StoreID, blockIDs, fileHeaders); err != nil { err := fmt.Errorf("failed to index file blocks") return &appError{err, "", http.StatusInternalServerError} } go updateSizePool.AddTask(repo.ID) return nil } func indexRawBlocks(repoID string, blockIDs []string, fileHeaders []*multipart.FileHeader) error { for i, handler := range fileHeaders { var buf bytes.Buffer f, err := handler.Open() if err != nil { err := fmt.Errorf("failed to open file for read: %v", err) return err } _, err = buf.ReadFrom(f) if err != nil { err := fmt.Errorf("failed to read block: %v", err) return err } checkSum := sha1.Sum(buf.Bytes()) blkID := hex.EncodeToString(checkSum[:]) if blkID != blockIDs[i] { err := fmt.Errorf("block id %s:%s doesn't match content", blkID, blockIDs[i]) return err } err = blockmgr.Write(repoID, blkID, &buf) if err != nil { err := fmt.Errorf("failed to write block: %s/%s: %v", repoID, blkID, err) return err } } return nil } func removeFileopExpireCache() { deleteBlockMaps := func(key interface{}, value interface{}) bool { if blkMap, ok := value.(*blockMap); ok { if blkMap.expireTime <= time.Now().Unix() { blockMapCacheTable.Delete(key) } } return true } blockMapCacheTable.Range(deleteBlockMaps) }