github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/data/replication.go (about) 1 package data 2 3 import ( 4 "log" 5 "net/http" 6 "strconv" 7 8 "github.com/cozy/cozy-stack/model/permission" 9 "github.com/cozy/cozy-stack/model/vfs" 10 "github.com/cozy/cozy-stack/pkg/config/config" 11 "github.com/cozy/cozy-stack/pkg/consts" 12 "github.com/cozy/cozy-stack/pkg/couchdb" 13 "github.com/cozy/cozy-stack/pkg/jsonapi" 14 "github.com/cozy/cozy-stack/web/middlewares" 15 "github.com/labstack/echo/v4" 16 ) 17 18 func proxy(c echo.Context, path string) error { 19 doctype := c.Param("doctype") 20 instance := middlewares.GetInstance(c) 21 p := couchdb.Proxy(instance, doctype, path) 22 logger := instance.Logger().WithNamespace("data-proxy").Writer() 23 defer logger.Close() 24 p.ErrorLog = log.New(logger, "", 0) 25 p.ServeHTTP(c.Response(), c.Request()) 26 return nil 27 } 28 29 func getLocalDoc(c echo.Context) error { 30 doctype := c.Param("doctype") 31 docid := c.Get("docid").(string) 32 33 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 34 return err 35 } 36 37 if err := permission.CheckReadable(doctype); err != nil { 38 return err 39 } 40 41 return proxy(c, "_local/"+docid) 42 } 43 44 func setLocalDoc(c echo.Context) error { 45 doctype := c.Param("doctype") 46 docid := c.Get("docid").(string) 47 48 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 49 return err 50 } 51 52 if err := permission.CheckReadable(doctype); err != nil { 53 return err 54 } 55 56 return proxy(c, "_local/"+docid) 57 } 58 59 func bulkGet(c echo.Context) error { 60 doctype := c.Param("doctype") 61 62 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 63 return err 64 } 65 66 if err := permission.CheckReadable(doctype); err != nil { 67 return err 68 } 69 70 return proxy(c, "_bulk_get") 71 } 72 73 func bulkDocs(c echo.Context) error { 74 doctype := c.Param("doctype") 75 76 if err := middlewares.AllowWholeType(c, permission.POST, doctype); err != nil { 77 return err 78 } 79 80 if err := permission.CheckWritable(doctype); err != nil { 81 return err 82 } 83 84 instance := middlewares.GetInstance(c) 85 if err := couchdb.EnsureDBExist(instance, doctype); err != nil { 86 return err 87 } 88 p, req, err := couchdb.ProxyBulkDocs(instance, doctype, c.Request()) 89 if err != nil { 90 var code int 91 if errHTTP, ok := err.(*echo.HTTPError); ok { 92 code = errHTTP.Code 93 } else { 94 code = http.StatusInternalServerError 95 } 96 return c.JSON(code, echo.Map{ 97 "error": err.Error(), 98 }) 99 } 100 101 p.ServeHTTP(c.Response(), req) 102 return nil 103 } 104 105 func createDB(c echo.Context) error { 106 doctype := c.Param("doctype") 107 108 if err := middlewares.AllowWholeType(c, permission.POST, doctype); err != nil { 109 return err 110 } 111 112 if err := permission.CheckWritable(doctype); err != nil { 113 return err 114 } 115 116 return proxy(c, "/") 117 } 118 119 func fullCommit(c echo.Context) error { 120 doctype := c.Param("doctype") 121 122 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 123 return err 124 } 125 126 if err := permission.CheckWritable(doctype); err != nil { 127 return err 128 } 129 130 return proxy(c, "_ensure_full_commit") 131 } 132 133 func revsDiff(c echo.Context) error { 134 doctype := c.Param("doctype") 135 136 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 137 return err 138 } 139 140 if err := permission.CheckReadable(doctype); err != nil { 141 return err 142 } 143 144 return proxy(c, "_revs_diff") 145 } 146 147 var allowedChangesParams = map[string]bool{ 148 "feed": true, 149 "style": true, 150 "since": true, 151 "limit": true, 152 "timeout": true, 153 "include_docs": true, 154 "heartbeat": true, // Pouchdb sends heartbeet even for non-continuous 155 "_nonce": true, // Pouchdb sends a request hash to avoid aggressive caching by some browsers 156 "seq_interval": true, 157 "descending": true, 158 } 159 160 func changesFeed(c echo.Context) error { 161 instance := middlewares.GetInstance(c) 162 doctype := c.Param("doctype") 163 164 // Drop a clear error for parameters not supported by stack 165 for key := range c.QueryParams() { 166 if key == "filter" && c.Request().Method == http.MethodPost { 167 continue 168 } 169 if !allowedChangesParams[key] { 170 return jsonapi.Errorf(http.StatusBadRequest, "Unsupported query parameter '%s'", key) 171 } 172 } 173 174 feed, err := couchdb.ValidChangesMode(c.QueryParam("feed")) 175 if err != nil { 176 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 177 } 178 179 feedStyle, err := couchdb.ValidChangesStyle(c.QueryParam("style")) 180 if err != nil { 181 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 182 } 183 184 filter, err := couchdb.StaticChangesFilter(c.QueryParam("filter")) 185 if err != nil { 186 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 187 } 188 189 limitString := c.QueryParam("limit") 190 limit := 0 191 if limitString != "" { 192 if limit, err = strconv.Atoi(limitString); err != nil { 193 return jsonapi.Errorf(http.StatusBadRequest, "Invalid limit value '%s': %s", limitString, err.Error()) 194 } 195 } 196 197 seqIntervalString := c.QueryParam("seq_interval") 198 seqInterval := 0 199 if seqIntervalString != "" { 200 if seqInterval, err = strconv.Atoi(seqIntervalString); err != nil { 201 return jsonapi.Errorf(http.StatusBadRequest, "Invalid seq_interval value '%s': %s", seqIntervalString, err.Error()) 202 } 203 } 204 205 includeDocs := paramIsTrue(c, "include_docs") 206 descending := paramIsTrue(c, "descending") 207 208 if err = permission.CheckReadable(doctype); err != nil { 209 return err 210 } 211 212 if err = middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 213 return err 214 } 215 216 // Use the VFS lock for the files to avoid sending the changed feed while 217 // the VFS is moving a directory. 218 if doctype == consts.Files { 219 mu := config.Lock().ReadWrite(instance, "vfs") 220 if err := mu.Lock(); err != nil { 221 return err 222 } 223 defer mu.Unlock() 224 } 225 226 couchReq := &couchdb.ChangesRequest{ 227 DocType: doctype, 228 Feed: feed, 229 Style: feedStyle, 230 Filter: filter, 231 Since: c.QueryParam("since"), 232 Limit: limit, 233 IncludeDocs: includeDocs, 234 SeqInterval: seqInterval, 235 Descending: descending, 236 } 237 238 var results *couchdb.ChangesResponse 239 if filter == "" { 240 results, err = couchdb.GetChanges(instance, couchReq) 241 } else { 242 results, err = couchdb.PostChanges(instance, couchReq, c.Request().Body) 243 } 244 if err != nil { 245 return err 246 } 247 248 if doctype == consts.Files { 249 if client, ok := middlewares.GetOAuthClient(c); ok { 250 err = vfs.FilterNotSynchronizedDocs(instance.VFS(), client.ID(), results) 251 if err != nil { 252 return err 253 } 254 } 255 } 256 257 return c.JSON(http.StatusOK, results) 258 } 259 260 func dbStatus(c echo.Context) error { 261 instance := middlewares.GetInstance(c) 262 doctype := c.Param("doctype") 263 264 if err := middlewares.AllowWholeType(c, permission.GET, doctype); err != nil { 265 return err 266 } 267 268 if err := permission.CheckReadable(doctype); err != nil { 269 return err 270 } 271 272 status, err := couchdb.DBStatus(instance, doctype) 273 if err != nil { 274 return err 275 } 276 277 return c.JSON(http.StatusOK, status) 278 } 279 280 func replicationRoutes(group *echo.Group) { 281 group.PUT("/", createDB) 282 283 // Routes used only for replication 284 group.GET("/", dbStatus) 285 group.GET("/_changes", changesFeed) 286 // POST=GET+filter see http://docs.couchdb.org/en/stable/api/database/changes.html#post--db-_changes) 287 group.POST("/_changes", changesFeed) 288 289 group.POST("/_ensure_full_commit", fullCommit) 290 291 // useful for Pouchdb replication 292 group.POST("/_bulk_get", bulkGet) // https://github.com/couchbase/sync_gateway/wiki/Bulk-GET 293 group.POST("/_bulk_docs", bulkDocs) 294 295 group.POST("/_revs_diff", revsDiff) 296 297 // for storing checkpoints 298 group.GET("/_local/:docid", getLocalDoc) 299 group.PUT("/_local/:docid", setLocalDoc) 300 }