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 }