From b1803fa51f61154065663bce83295b4c5360e2bb Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Fri, 22 Apr 2022 15:57:21 +0800 Subject: [PATCH] fix: cannot overwrite file to slave policy / fix: remove lock system for webdav to resolve Windows Explorer issue. --- pkg/filesystem/driver/remote/client.go | 14 +- pkg/filesystem/driver/remote/handler.go | 2 +- pkg/webdav/webdav.go | 435 +++++++++++++----------- routers/controllers/webdav.go | 2 + service/explorer/slave.go | 7 +- 5 files changed, 246 insertions(+), 214 deletions(-) diff --git a/pkg/filesystem/driver/remote/client.go b/pkg/filesystem/driver/remote/client.go index 2f267eb..b1b1804 100644 --- a/pkg/filesystem/driver/remote/client.go +++ b/pkg/filesystem/driver/remote/client.go @@ -30,7 +30,7 @@ const ( // Client to operate uploading to remote slave server type Client interface { // CreateUploadSession creates remote upload session - CreateUploadSession(ctx context.Context, session *serializer.UploadSession, ttl int64) error + CreateUploadSession(ctx context.Context, session *serializer.UploadSession, ttl int64, overwrite bool) error // GetUploadURL signs an url for uploading file GetUploadURL(ttl int64, sessionID string) (string, string, error) // Upload uploads file to remote server @@ -82,12 +82,11 @@ func (c *remoteClient) Upload(ctx context.Context, file fsctx.FileHeader) error } // Create upload session - if err := c.CreateUploadSession(ctx, session, int64(ttl)); err != nil { + overwrite := fileInfo.Mode&fsctx.Overwrite == fsctx.Overwrite + if err := c.CreateUploadSession(ctx, session, int64(ttl), overwrite); err != nil { return fmt.Errorf("failed to create upload session: %w", err) } - overwrite := fileInfo.Mode&fsctx.Overwrite == fsctx.Overwrite - // Initial chunk groups chunks := chunk.NewChunkGroup(file, c.policy.OptionsSerialized.ChunkSize, &backoff.ConstantBackoff{ Max: model.GetIntSetting("chunk_retries", 5), @@ -130,10 +129,11 @@ func (c *remoteClient) DeleteUploadSession(ctx context.Context, sessionID string return nil } -func (c *remoteClient) CreateUploadSession(ctx context.Context, session *serializer.UploadSession, ttl int64) error { +func (c *remoteClient) CreateUploadSession(ctx context.Context, session *serializer.UploadSession, ttl int64, overwrite bool) error { reqBodyEncoded, err := json.Marshal(map[string]interface{}{ - "session": session, - "ttl": ttl, + "session": session, + "ttl": ttl, + "overwrite": overwrite, }) if err != nil { return err diff --git a/pkg/filesystem/driver/remote/handler.go b/pkg/filesystem/driver/remote/handler.go index 8837439..7594753 100644 --- a/pkg/filesystem/driver/remote/handler.go +++ b/pkg/filesystem/driver/remote/handler.go @@ -281,7 +281,7 @@ func (handler *Driver) Token(ctx context.Context, ttl int64, uploadSession *seri // 在从机端创建上传会话 uploadSession.Callback = apiURL.String() - if err := handler.uploadClient.CreateUploadSession(ctx, uploadSession, ttl); err != nil { + if err := handler.uploadClient.CreateUploadSession(ctx, uploadSession, ttl, false); err != nil { return nil, err } diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index 770b352..8b248e4 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -14,6 +14,7 @@ import ( "path" "strconv" "strings" + "sync" "time" model "github.com/cloudreve/Cloudreve/v3/models" @@ -30,6 +31,7 @@ type Handler struct { // Logger is an optional error logger. If non-nil, it will be called // for all HTTP requests. Logger func(*http.Request, error) + Mutex *sync.Mutex } func (h *Handler) stripPrefix(p string, uid uint) (string, int, error) { @@ -60,13 +62,19 @@ func isPathExist(ctx context.Context, fs *filesystem.FileSystem, path string) (b func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) { status, err := http.StatusBadRequest, errUnsupportedMethod + h.Mutex.Lock() if h.LockSystem == nil { + h.Mutex.Unlock() status, err = http.StatusInternalServerError, errNoLockSystem } else { - // 检查并新建LockSystem - if _, ok := h.LockSystem[fs.User.ID]; !ok { + // 检查并新建 LockSystem + ls, ok := h.LockSystem[fs.User.ID] + if !ok { h.LockSystem[fs.User.ID] = NewMemLS() + ls = h.LockSystem[fs.User.ID] } + h.Mutex.Unlock() + switch r.Method { case "OPTIONS": status, err = h.handleOptions(w, r, fs) @@ -81,13 +89,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, fs *filesyst case "COPY", "MOVE": status, err = h.handleCopyMove(w, r, fs) case "LOCK": - status, err = h.handleLock(w, r, fs) + status, err = h.handleLock(w, r, fs, ls) case "UNLOCK": - status, err = h.handleUnlock(w, r, fs) + status, err = h.handleUnlock(w, r, fs, ls) case "PROPFIND": - status, err = h.handlePropfind(w, r, fs) + status, err = h.handlePropfind(w, r, fs, ls) case "PROPPATCH": - status, err = h.handleProppatch(w, r, fs) + status, err = h.handleProppatch(w, r, fs, ls) } } @@ -103,98 +111,111 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, fs *filesyst } // OK -func (h *Handler) lock(now time.Time, root string, fs *filesystem.FileSystem) (token string, status int, err error) { - token, err = h.LockSystem[fs.User.ID].Create(now, LockDetails{ - Root: root, - Duration: infiniteTimeout, - ZeroDepth: true, - }) - if err != nil { - if err == ErrLocked { - return "", StatusLocked, err - } - return "", http.StatusInternalServerError, err - } - return token, 0, nil +func (h *Handler) lock(now time.Time, root string, fs *filesystem.FileSystem, ls LockSystem) (token string, status int, err error) { + //token, err = ls.Create(now, LockDetails{ + // Root: root, + // Duration: infiniteTimeout, + // ZeroDepth: true, + //}) + //if err != nil { + // if err == ErrLocked { + // return "", StatusLocked, err + // } + // return "", http.StatusInternalServerError, err + //} + + return fmt.Sprintf("%d", time.Now().Unix()), 0, nil } // ok func (h *Handler) confirmLocks(r *http.Request, src, dst string, fs *filesystem.FileSystem) (release func(), status int, err error) { - hdr := r.Header.Get("If") - if hdr == "" { - // An empty If header means that the client hasn't previously created locks. - // Even if this client doesn't care about locks, we still need to check that - // the resources aren't locked by another client, so we create temporary - // locks that would conflict with another client's locks. These temporary - // locks are unlocked at the end of the HTTP request. - now, srcToken, dstToken := time.Now(), "", "" - if src != "" { - srcToken, status, err = h.lock(now, src, fs) - if err != nil { - return nil, status, err - } - } - if dst != "" { - dstToken, status, err = h.lock(now, dst, fs) - if err != nil { - if srcToken != "" { - h.LockSystem[fs.User.ID].Unlock(now, srcToken) - } - return nil, status, err - } - } - return func() { - if dstToken != "" { - h.LockSystem[fs.User.ID].Unlock(now, dstToken) - } - if srcToken != "" { - h.LockSystem[fs.User.ID].Unlock(now, srcToken) - } - }, 0, nil - } + //hdr := r.Header.Get("If") + //h.Mutex.Lock() + //ls,ok := h.LockSystem[fs.User.ID] + //h.Mutex.Unlock() + //if !ok{ + // return nil, http.StatusInternalServerError, errNoLockSystem + //} + // + //if hdr == "" { + // // An empty If header means that the client hasn't previously created locks. + // // Even if this client doesn't care about locks, we still need to check that + // // the resources aren't locked by another client, so we create temporary + // // locks that would conflict with another client's locks. These temporary + // // locks are unlocked at the end of the HTTP request. + // now, srcToken, dstToken := time.Now(), "", "" + // if src != "" { + // srcToken, status, err = h.lock(now, src, fs,ls) + // if err != nil { + // return nil, status, err + // } + // } + // if dst != "" { + // dstToken, status, err = h.lock(now, dst, fs,ls) + // if err != nil { + // if srcToken != "" { + // ls.Unlock(now, srcToken) + // } + // return nil, status, err + // } + // } + // + // return func() { + // if dstToken != "" { + // ls.Unlock(now, dstToken) + // } + // if srcToken != "" { + // ls.Unlock(now, srcToken) + // } + // }, 0, nil + //} + // + //ih, ok := parseIfHeader(hdr) + //if !ok { + // return nil, http.StatusBadRequest, errInvalidIfHeader + //} + //// ih is a disjunction (OR) of ifLists, so any ifList will do. + //for _, l := range ih.lists { + // lsrc := l.resourceTag + // if lsrc == "" { + // lsrc = src + // } else { + // u, err := url.Parse(lsrc) + // if err != nil { + // continue + // } + // //if u.Host != r.Host { + // // continue + // //} + // lsrc, status, err = h.stripPrefix(u.Path, fs.User.ID) + // if err != nil { + // return nil, status, err + // } + // } + // release, err = ls.Confirm( + // time.Now(), + // lsrc, + // dst, + // l.conditions..., + // ) + // if err == ErrConfirmationFailed { + // continue + // } + // if err != nil { + // return nil, http.StatusInternalServerError, err + // } + // return release, 0, nil + //} + //// Section 10.4.1 says that "If this header is evaluated and all state lists + //// fail, then the request must fail with a 412 (Precondition Failed) status." + //// We follow the spec even though the cond_put_corrupt_token test case from + //// the litmus test warns on seeing a 412 instead of a 423 (Locked). + //return nil, http.StatusPreconditionFailed, ErrLocked - ih, ok := parseIfHeader(hdr) - if !ok { - return nil, http.StatusBadRequest, errInvalidIfHeader - } - // ih is a disjunction (OR) of ifLists, so any ifList will do. - for _, l := range ih.lists { - lsrc := l.resourceTag - if lsrc == "" { - lsrc = src - } else { - u, err := url.Parse(lsrc) - if err != nil { - continue - } - //if u.Host != r.Host { - // continue - //} - lsrc, status, err = h.stripPrefix(u.Path, fs.User.ID) - if err != nil { - return nil, status, err - } - } - release, err = h.LockSystem[fs.User.ID].Confirm( - time.Now(), - lsrc, - dst, - l.conditions..., - ) - if err == ErrConfirmationFailed { - continue - } - if err != nil { - return nil, http.StatusInternalServerError, err - } - return release, 0, nil - } - // Section 10.4.1 says that "If this header is evaluated and all state lists - // fail, then the request must fail with a 412 (Precondition Failed) status." - // We follow the spec even though the cond_put_corrupt_token test case from - // the litmus test warns on seeing a 412 instead of a 423 (Locked). - return nil, http.StatusPreconditionFailed, ErrLocked + return func() { + + }, 0, nil } //OK @@ -245,7 +266,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request, fs * return http.StatusInternalServerError, err } - etag, err := findETag(ctx, fs, h.LockSystem[fs.User.ID], reqPath, &fs.FileTarget[0]) + etag, err := findETag(ctx, fs, nil, reqPath, &fs.FileTarget[0]) if err != nil { return http.StatusInternalServerError, err } @@ -271,6 +292,7 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, fs *files if err != nil { return status, err } + release, status, err := h.confirmLocks(r, reqPath, "", fs) if err != nil { return status, err @@ -373,7 +395,7 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst return http.StatusMethodNotAllowed, err } - etag, err := findETag(ctx, fs, h.LockSystem[fs.User.ID], reqPath, fileData.Model.(*model.File)) + etag, err := findETag(ctx, fs, nil, reqPath, fileData.Model.(*model.File)) if err != nil { return http.StatusInternalServerError, err } @@ -494,130 +516,137 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, fs *fil } // OK -func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (retStatus int, retErr error) { +func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem, ls LockSystem) (retStatus int, retErr error) { defer fs.Recycle() duration, err := parseTimeout(r.Header.Get("Timeout")) if err != nil { return http.StatusBadRequest, err } - li, status, err := readLockInfo(r.Body) + + reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID) if err != nil { return status, err } - //ctx := r.Context() - token, ld, now, created := "", LockDetails{}, time.Now(), false - if li == (lockInfo{}) { - // An empty lockInfo means to refresh the lock. - ih, ok := parseIfHeader(r.Header.Get("If")) - if !ok { - return http.StatusBadRequest, errInvalidIfHeader - } - if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { - token = ih.lists[0].conditions[0].Token - } - if token == "" { - return http.StatusBadRequest, errInvalidLockToken - } - ld, err = h.LockSystem[fs.User.ID].Refresh(now, token, duration) - if err != nil { - if err == ErrNoSuchLock { - return http.StatusPreconditionFailed, err - } - return http.StatusInternalServerError, err - } + ////ctx := r.Context() + //token, ld, now, created := "", LockDetails{}, time.Now(), false + //if li == (lockInfo{}) { + // // An empty lockInfo means to refresh the lock. + // ih, ok := parseIfHeader(r.Header.Get("If")) + // if !ok { + // return http.StatusBadRequest, errInvalidIfHeader + // } + // if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { + // token = ih.lists[0].conditions[0].Token + // } + // if token == "" { + // return http.StatusBadRequest, errInvalidLockToken + // } + // ld, err = ls.Refresh(now, token, duration) + // if err != nil { + // if err == ErrNoSuchLock { + // return http.StatusPreconditionFailed, err + // } + // return http.StatusInternalServerError, err + // } + // + //} else { + // // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, + // // then the request MUST act as if a "Depth:infinity" had been submitted." + // depth := infiniteDepth + // if hdr := r.Header.Get("Depth"); hdr != "" { + // depth = parseDepth(hdr) + // if depth != 0 && depth != infiniteDepth { + // // Section 9.10.3 says that "Values other than 0 or infinity must not be + // // used with the Depth header on a LOCK method". + // return http.StatusBadRequest, errInvalidDepth + // } + // } + // reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID) + // if err != nil { + // return status, err + // } + // ld = LockDetails{ + // Root: reqPath, + // Duration: duration, + // OwnerXML: li.Owner.InnerXML, + // ZeroDepth: depth == 0, + // } + // token, err = ls.Create(now, ld) + // if err != nil { + // if err == ErrLocked { + // return StatusLocked, err + // } + // return http.StatusInternalServerError, err + // } + // defer func() { + // if retErr != nil { + // ls.Unlock(now, token) + // } + // }() + // + // // Create the resource if it didn't previously exist. + // //if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { + // // f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + // // if err != nil { + // // // TODO: detect missing intermediate dirs and return http.StatusConflict? + // // return http.StatusInternalServerError, err + // // } + // // f.Close() + // // created = true + // //} + // + // // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + // // Lock-Token value is a Coded-URL. We add angle brackets. + // w.Header().Set("Lock-Token", "<"+token+">") + //} + // + //w.Header().Set("Content-Type", "application/xml; charset=utf-8") + //if created { + // // This is "w.WriteHeader(http.StatusCreated)" and not "return + // // http.StatusCreated, nil" because we write our own (XML) response to w + // // and Handler.ServeHTTP would otherwise write "Created". + // w.WriteHeader(http.StatusCreated) + //} - } else { - // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, - // then the request MUST act as if a "Depth:infinity" had been submitted." - depth := infiniteDepth - if hdr := r.Header.Get("Depth"); hdr != "" { - depth = parseDepth(hdr) - if depth != 0 && depth != infiniteDepth { - // Section 9.10.3 says that "Values other than 0 or infinity must not be - // used with the Depth header on a LOCK method". - return http.StatusBadRequest, errInvalidDepth - } - } - reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID) - if err != nil { - return status, err - } - ld = LockDetails{ - Root: reqPath, - Duration: duration, - OwnerXML: li.Owner.InnerXML, - ZeroDepth: depth == 0, - } - token, err = h.LockSystem[fs.User.ID].Create(now, ld) - if err != nil { - if err == ErrLocked { - return StatusLocked, err - } - return http.StatusInternalServerError, err - } - defer func() { - if retErr != nil { - h.LockSystem[fs.User.ID].Unlock(now, token) - } - }() - - // Create the resource if it didn't previously exist. - //if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { - // f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) - // if err != nil { - // // TODO: detect missing intermediate dirs and return http.StatusConflict? - // return http.StatusInternalServerError, err - // } - // f.Close() - // created = true - //} - - // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the - // Lock-Token value is a Coded-URL. We add angle brackets. - w.Header().Set("Lock-Token", "<"+token+">") - } - - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - if created { - // This is "w.WriteHeader(http.StatusCreated)" and not "return - // http.StatusCreated, nil" because we write our own (XML) response to w - // and Handler.ServeHTTP would otherwise write "Created". - w.WriteHeader(http.StatusCreated) - } - writeLockInfo(w, token, ld) + writeLockInfo(w, fmt.Sprintf("%d", time.Now().UnixNano()), LockDetails{ + Duration: duration, + OwnerXML: fs.User.Email, + Root: reqPath, + }) return 0, nil } // OK -func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (status int, err error) { +func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem, ls LockSystem) (status int, err error) { defer fs.Recycle() + return http.StatusNoContent, err - // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the - // Lock-Token value is a Coded-URL. We strip its angle brackets. - t := r.Header.Get("Lock-Token") - if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { - return http.StatusBadRequest, errInvalidLockToken - } - t = t[1 : len(t)-1] - - switch err = h.LockSystem[fs.User.ID].Unlock(time.Now(), t); err { - case nil: - return http.StatusNoContent, err - case ErrForbidden: - return http.StatusForbidden, err - case ErrLocked: - return StatusLocked, err - case ErrNoSuchLock: - return http.StatusConflict, err - default: - return http.StatusInternalServerError, err - } + //// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + //// Lock-Token value is a Coded-URL. We strip its angle brackets. + //t := r.Header.Get("Lock-Token") + //if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { + // return http.StatusBadRequest, errInvalidLockToken + //} + //t = t[1 : len(t)-1] + // + //switch err = ls.Unlock(time.Now(), t); err { + //case nil: + // return http.StatusNoContent, err + //case ErrForbidden: + // return http.StatusForbidden, err + //case ErrLocked: + // return StatusLocked, err + //case ErrNoSuchLock: + // return http.StatusConflict, err + //default: + // return http.StatusInternalServerError, err + //} } // OK -func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (status int, err error) { +func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem, ls LockSystem) (status int, err error) { defer fs.Recycle() reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID) @@ -651,7 +680,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, fs *fil } var pstats []Propstat if pf.Propname != nil { - pnames, err := propnames(ctx, fs, h.LockSystem[fs.User.ID], info) + pnames, err := propnames(ctx, fs, ls, info) if err != nil { return err } @@ -661,9 +690,9 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, fs *fil } pstats = append(pstats, pstat) } else if pf.Allprop != nil { - pstats, err = allprop(ctx, fs, h.LockSystem[fs.User.ID], info, pf.Prop) + pstats, err = allprop(ctx, fs, ls, info, pf.Prop) } else { - pstats, err = props(ctx, fs, h.LockSystem[fs.User.ID], info, pf.Prop) + pstats, err = props(ctx, fs, ls, info, pf.Prop) } if err != nil { return err @@ -686,7 +715,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, fs *fil return 0, nil } -func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (status int, err error) { +func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem, ls LockSystem) (status int, err error) { defer fs.Recycle() reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID) @@ -708,7 +737,7 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request, fs *fi if err != nil { return status, err } - pstats, err := patch(ctx, fs, h.LockSystem[fs.User.ID], reqPath, patches) + pstats, err := patch(ctx, fs, ls, reqPath, patches) if err != nil { return http.StatusInternalServerError, err } diff --git a/routers/controllers/webdav.go b/routers/controllers/webdav.go index daf10e7..991bcf1 100644 --- a/routers/controllers/webdav.go +++ b/routers/controllers/webdav.go @@ -7,6 +7,7 @@ import ( "github.com/cloudreve/Cloudreve/v3/pkg/webdav" "github.com/cloudreve/Cloudreve/v3/service/setting" "github.com/gin-gonic/gin" + "sync" ) var handler *webdav.Handler @@ -15,6 +16,7 @@ func init() { handler = &webdav.Handler{ Prefix: "/dav", LockSystem: make(map[uint]webdav.LockSystem), + Mutex: &sync.Mutex{}, } } diff --git a/service/explorer/slave.go b/service/explorer/slave.go index 32180b8..f2ba487 100644 --- a/service/explorer/slave.go +++ b/service/explorer/slave.go @@ -167,13 +167,14 @@ func CreateTransferTask(c *gin.Context, req *serializer.SlaveTransferReq) serial // SlaveListService 从机上传会话服务 type SlaveCreateUploadSessionService struct { - Session serializer.UploadSession `json:"session" binding:"required"` - TTL int64 `json:"ttl"` + Session serializer.UploadSession `json:"session" binding:"required"` + TTL int64 `json:"ttl"` + Overwrite bool `json:"overwrite"` } // Create 从机创建上传会话 func (service *SlaveCreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response { - if util.Exists(service.Session.SavePath) { + if !service.Overwrite && util.Exists(service.Session.SavePath) { return serializer.Err(serializer.CodeConflict, "placeholder file already exist", nil) }