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  }