github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/index.go (about)

     1  package couchdb
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/cozy/cozy-stack/pkg/consts"
     9  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    10  	"github.com/cozy/cozy-stack/pkg/logger"
    11  	"github.com/cozy/cozy-stack/pkg/prefixer"
    12  	"golang.org/x/sync/errgroup"
    13  )
    14  
    15  // IndexViewsVersion is the version of current definition of views & indexes.
    16  // This number should be incremented when this file changes.
    17  const IndexViewsVersion int = 37
    18  
    19  // Indexes is the index list required by an instance to run properly.
    20  var Indexes = []*mango.Index{
    21  	// Permissions
    22  	mango.MakeIndex(consts.Permissions, "by-source-and-type", mango.IndexDef{Fields: []string{"source_id", "type"}}),
    23  
    24  	// Used to lookup over the children of a directory
    25  	mango.MakeIndex(consts.Files, "dir-children", mango.IndexDef{Fields: []string{"dir_id", "_id"}}),
    26  	// Used to lookup a directory given its path
    27  	mango.MakeIndex(consts.Files, "dir-by-path", mango.IndexDef{Fields: []string{"path"}}),
    28  	// Used to find notes
    29  	mango.MakeIndex(consts.Files, "by-mime-updated-at", mango.IndexDef{Fields: []string{"mime", "trashed", "updated_at"}}),
    30  	// Used by the FSCK to detect conflicts
    31  	mango.MakeIndex(consts.Files, "with-conflicts", mango.IndexDef{Fields: []string{"_conflicts"}}),
    32  	// Used to count the shortuts to a sharing that have not been seen
    33  	mango.MakeIndex(consts.Files, "by-sharing-status", mango.IndexDef{Fields: []string{"metadata.sharing.status"}}),
    34  	// Used to find old files and directories in the trashed that should be deleted
    35  	mango.MakeIndex(consts.Files, "by-dir-id-updated-at", mango.IndexDef{Fields: []string{"dir_id", "updated_at"}}),
    36  
    37  	// Used to lookup a queued and running jobs
    38  	mango.MakeIndex(consts.Jobs, "by-worker-and-state", mango.IndexDef{Fields: []string{"worker", "state"}}),
    39  	mango.MakeIndex(consts.Jobs, "by-trigger-id", mango.IndexDef{Fields: []string{"trigger_id", "queued_at"}}),
    40  	mango.MakeIndex(consts.Jobs, "by-queued-at", mango.IndexDef{Fields: []string{"queued_at"}}),
    41  
    42  	// Used to lookup a trigger to see if it exists or must be created
    43  	mango.MakeIndex(consts.Triggers, "by-worker-and-type", mango.IndexDef{Fields: []string{"worker", "type"}}),
    44  
    45  	// Used to lookup oauth clients by name
    46  	mango.MakeIndex(consts.OAuthClients, "by-client-name", mango.IndexDef{Fields: []string{"client_name"}}),
    47  	mango.MakeIndex(consts.OAuthClients, "by-notification-platform", mango.IndexDef{Fields: []string{"notification_platform"}}),
    48  	mango.MakeIndex(consts.OAuthClients, "connected-user-clients", mango.IndexDef{
    49  		Fields: []string{"client_kind", "client_name"},
    50  		PartialFilter: mango.And(
    51  			mango.In("client_kind", []interface{}{"browser", "desktop", "mobile"}),
    52  			mango.NotExists("pending"),
    53  		),
    54  	}),
    55  
    56  	// Used to lookup login history by OS, browser, and IP
    57  	mango.MakeIndex(consts.SessionsLogins, "by-os-browser-ip", mango.IndexDef{Fields: []string{"os", "browser", "ip"}}),
    58  
    59  	// Used to lookup notifications by their source, ordered by their creation
    60  	// date
    61  	mango.MakeIndex(consts.Notifications, "by-source-id", mango.IndexDef{Fields: []string{"source_id", "created_at"}}),
    62  
    63  	// Used to find the myself document
    64  	mango.MakeIndex(consts.Contacts, "by-me", mango.IndexDef{Fields: []string{"me"}}),
    65  
    66  	// Used to lookup the bitwarden ciphers
    67  	mango.MakeIndex(consts.BitwardenCiphers, "by-folder-id", mango.IndexDef{Fields: []string{"folder_id"}}),
    68  	mango.MakeIndex(consts.BitwardenCiphers, "by-organization-id", mango.IndexDef{Fields: []string{"organization_id"}}),
    69  
    70  	// Used to find the contacts in a group
    71  	mango.MakeIndex(consts.Contacts, "by-groups", mango.IndexDef{Fields: []string{"relationships.groups.data"}}),
    72  
    73  	// Used to find the active sharings
    74  	mango.MakeIndex(consts.Sharings, "active", mango.IndexDef{Fields: []string{"active"}}),
    75  }
    76  
    77  // DiskUsageView is the view used for computing the disk usage for files
    78  var DiskUsageView = &View{
    79  	Name:    "disk-usage",
    80  	Doctype: consts.Files,
    81  	Map: `
    82  function(doc) {
    83    if (doc.type === 'file') {
    84      emit(doc.dir_id, +doc.size);
    85    }
    86  }
    87  `,
    88  	Reduce: "_sum",
    89  }
    90  
    91  // OldVersionsDiskUsageView is the view used for computing the disk usage for
    92  // the old versions of file contents.
    93  var OldVersionsDiskUsageView = &View{
    94  	Name:    "old-versions-disk-usage",
    95  	Doctype: consts.FilesVersions,
    96  	Map: `
    97  function(doc) {
    98    emit(doc._id, +doc.size);
    99  }
   100  `,
   101  	Reduce: "_sum",
   102  }
   103  
   104  // DirNotSynchronizedOnView is the view used for fetching directories that are
   105  // not synchronized on a given device.
   106  var DirNotSynchronizedOnView = &View{
   107  	Name:    "not-synchronized-on",
   108  	Doctype: consts.Files,
   109  	Reduce:  "_count",
   110  	Map: `
   111  function(doc) {
   112    if (doc.type === "directory" && isArray(doc.not_synchronized_on)) {
   113      for (var i = 0; i < doc.not_synchronized_on.length; i++) {
   114        emit([doc.not_synchronized_on[i].type, doc.not_synchronized_on[i].id]);
   115      }
   116    }
   117  }`,
   118  }
   119  
   120  // FilesReferencedByView is the view used for fetching files referenced by a
   121  // given document
   122  var FilesReferencedByView = &View{
   123  	Name:    "referenced-by",
   124  	Doctype: consts.Files,
   125  	Reduce:  "_count",
   126  	Map: `
   127  function(doc) {
   128    if (isArray(doc.referenced_by)) {
   129      for (var i = 0; i < doc.referenced_by.length; i++) {
   130        emit([doc.referenced_by[i].type, doc.referenced_by[i].id]);
   131      }
   132    }
   133  }`,
   134  }
   135  
   136  // ReferencedBySortedByDatetimeView is the view used for fetching files referenced by a
   137  // given document, sorted by the datetime
   138  var ReferencedBySortedByDatetimeView = &View{
   139  	Name:    "referenced-by-sorted-by-datetime",
   140  	Doctype: consts.Files,
   141  	Reduce:  "_count",
   142  	Map: `
   143  function(doc) {
   144    if (isArray(doc.referenced_by)) {
   145      for (var i = 0; i < doc.referenced_by.length; i++) {
   146        var datetime = (doc.metadata && doc.metadata.datetime) || '';
   147        emit([doc.referenced_by[i].type, doc.referenced_by[i].id, datetime]);
   148      }
   149    }
   150  }`,
   151  }
   152  
   153  // FilesByParentView is the view used for fetching files referenced by a
   154  // given document
   155  var FilesByParentView = &View{
   156  	Name:    "by-parent-type-name",
   157  	Doctype: consts.Files,
   158  	Map: `
   159  function(doc) {
   160    emit([doc.dir_id, doc.type, doc.name])
   161  }`,
   162  	Reduce: "_count",
   163  }
   164  
   165  // PermissionsShareByCView is the view for fetching the permissions associated
   166  // to a document via a token code.
   167  var PermissionsShareByCView = &View{
   168  	Name:    "byToken",
   169  	Doctype: consts.Permissions,
   170  	Map: `
   171  function(doc) {
   172    if (doc.type && doc.type.slice(0, 5) === "share" && doc.codes) {
   173      Object.keys(doc.codes).forEach(function(k) {
   174        emit(doc.codes[k]);
   175      })
   176    }
   177  }`,
   178  }
   179  
   180  // PermissionsShareByShortcodeView is the view for fetching the permissions associated
   181  // to a document via a token code.
   182  var PermissionsShareByShortcodeView = &View{
   183  	Name:    "by-short-code",
   184  	Doctype: consts.Permissions,
   185  	Map: `
   186  function(doc) {
   187  	if(doc.shortcodes) {
   188  		for(var idx in doc.shortcodes) {
   189  			emit(doc.shortcodes[idx], idx);
   190  		}
   191  	}
   192  }`,
   193  }
   194  
   195  // PermissionsShareByDocView is the view for fetching a list of permissions
   196  // associated to a list of IDs.
   197  var PermissionsShareByDocView = &View{
   198  	Name:    "byDoc",
   199  	Doctype: consts.Permissions,
   200  	Map: `
   201  function(doc) {
   202    if (doc.type === "share" && doc.permissions) {
   203      Object.keys(doc.permissions).forEach(function(k) {
   204        var p = doc.permissions[k];
   205        var selector = p.selector || "_id";
   206        for (var i=0; i<p.values.length; i++) {
   207          emit([p.type, selector, p.values[i]], p.verbs);
   208        }
   209      });
   210    }
   211  }`,
   212  }
   213  
   214  // PermissionsByDoctype returns a list of permissions that have at least one
   215  // rule for the given doctype.
   216  var PermissionsByDoctype = &View{
   217  	Name:    "permissions-by-doctype",
   218  	Doctype: consts.Permissions,
   219  	Map: `
   220  function(doc) {
   221    if (doc.permissions) {
   222      Object.keys(doc.permissions).forEach(function(k) {
   223  	  emit([doc.permissions[k].type, doc.type]);
   224  	});
   225    }
   226  }
   227  `,
   228  }
   229  
   230  // SharedDocsBySharingID is the view for fetching a list of shared doctype/id
   231  // associated with a sharingid
   232  var SharedDocsBySharingID = &View{
   233  	Name:    "shared-docs-by-sharingid",
   234  	Doctype: consts.Shared,
   235  	Map: `
   236  function(doc) {
   237    if (doc.infos) {
   238      Object.keys(doc.infos).forEach(function(k) {
   239        emit(k, doc._id);
   240      });
   241    }
   242  }`,
   243  }
   244  
   245  // SharingsByDocTypeView is the view for fetching a list of sharings
   246  // associated with a doctype
   247  var SharingsByDocTypeView = &View{
   248  	Name:    "sharings-by-doctype",
   249  	Doctype: consts.Sharings,
   250  	Map: `
   251  function(doc) {
   252  	if (isArray(doc.rules)) {
   253  		for (var i = 0; i < doc.rules.length; i++) {
   254  			if (!doc.rules[i].local) {
   255  				emit(doc.rules[i].doctype, doc._id);
   256  			}
   257  		}
   258  	}
   259  }`,
   260  }
   261  
   262  // ContactByEmail is used to find a contact by its email address
   263  var ContactByEmail = &View{
   264  	Name:    "contacts-by-email",
   265  	Doctype: consts.Contacts,
   266  	Map: `
   267  function(doc) {
   268  	if (isArray(doc.email)) {
   269  		for (var i = 0; i < doc.email.length; i++) {
   270  			emit(doc.email[i].address, doc._id);
   271  		}
   272  	}
   273  }
   274  `,
   275  }
   276  
   277  // Views is the list of all views that are created by the stack.
   278  var Views = []*View{
   279  	DiskUsageView,
   280  	OldVersionsDiskUsageView,
   281  	DirNotSynchronizedOnView,
   282  	FilesReferencedByView,
   283  	ReferencedBySortedByDatetimeView,
   284  	FilesByParentView,
   285  	PermissionsShareByCView,
   286  	PermissionsShareByDocView,
   287  	PermissionsByDoctype,
   288  	PermissionsShareByShortcodeView,
   289  	SharedDocsBySharingID,
   290  	SharingsByDocTypeView,
   291  	ContactByEmail,
   292  }
   293  
   294  // ViewsByDoctype returns the list of views for a specified doc type.
   295  func ViewsByDoctype(doctype string) []*View {
   296  	var views []*View
   297  	for _, view := range Views {
   298  		if view.Doctype == doctype {
   299  			views = append(views, view)
   300  		}
   301  	}
   302  	return views
   303  }
   304  
   305  // IndexesByDoctype returns the list of indexes for a specified doc type.
   306  func IndexesByDoctype(doctype string) []*mango.Index {
   307  	var indexes []*mango.Index
   308  	for _, index := range Indexes {
   309  		if index.Doctype == doctype {
   310  			indexes = append(indexes, index)
   311  		}
   312  	}
   313  	return indexes
   314  }
   315  
   316  // globalIndexes is the index list required on the global databases to run
   317  // properly.
   318  var globalIndexes = []*mango.Index{
   319  	mango.MakeIndex(consts.Exports, "by-domain", mango.IndexDef{Fields: []string{"domain", "created_at"}}),
   320  }
   321  
   322  // secretIndexes is the index list required on the secret databases to run
   323  // properly
   324  var secretIndexes = []*mango.Index{
   325  	mango.MakeIndex(consts.AccountTypes, "by-slug", mango.IndexDef{Fields: []string{"slug"}}),
   326  }
   327  
   328  // DomainAndAliasesView defines a view to fetch instances by domain and domain
   329  // aliases.
   330  var DomainAndAliasesView = &View{
   331  	Name:    "domain-and-aliases",
   332  	Doctype: consts.Instances,
   333  	Map: `
   334  function(doc) {
   335    emit(doc.domain);
   336    if (isArray(doc.domain_aliases)) {
   337      for (var i = 0; i < doc.domain_aliases.length; i++) {
   338        emit(doc.domain_aliases[i]);
   339      }
   340    }
   341  }
   342  `,
   343  }
   344  
   345  // globalViews is the list of all views that are created by the stack on the
   346  // global databases.
   347  var globalViews = []*View{
   348  	DomainAndAliasesView,
   349  }
   350  
   351  // InitGlobalDB defines views and indexes on the global databases. It is called
   352  // on every startup of the stack.
   353  func InitGlobalDB(ctx context.Context) error {
   354  	var err error
   355  	// Check that we can properly reach CouchDB.
   356  	attempts := 8
   357  	attemptsSpacing := 1 * time.Second
   358  	for i := 0; i < attempts; i++ {
   359  		_, err = CheckStatus(ctx)
   360  		if err == nil {
   361  			break
   362  		}
   363  
   364  		err = fmt.Errorf("could not reach Couchdb database: %w", err)
   365  		if i < attempts-1 {
   366  			logger.WithNamespace("stack").Warnf("%s, retrying in %v", err, attemptsSpacing)
   367  			time.Sleep(attemptsSpacing)
   368  		}
   369  	}
   370  
   371  	if err != nil {
   372  		return fmt.Errorf("failed contact couchdb: %w", err)
   373  	}
   374  
   375  	g, _ := errgroup.WithContext(context.Background())
   376  
   377  	DefineIndexes(g, prefixer.SecretsPrefixer, secretIndexes)
   378  	DefineIndexes(g, prefixer.GlobalPrefixer, globalIndexes)
   379  	DefineViews(g, prefixer.GlobalPrefixer, globalViews)
   380  
   381  	return g.Wait()
   382  }
   383  
   384  // CheckDesignDocCanBeDeleted will return false for an index or view used by
   385  // the stack.
   386  func CheckDesignDocCanBeDeleted(doctype, name string) bool {
   387  	for _, index := range Indexes {
   388  		if doctype == index.Doctype && name == index.Request.DDoc {
   389  			return false
   390  		}
   391  	}
   392  	for _, view := range Views {
   393  		if doctype == view.Doctype && name == view.Name {
   394  			return false
   395  		}
   396  	}
   397  	return true
   398  }