github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/app/konnector.go (about) 1 package app 2 3 import ( 4 "encoding/json" 5 "io" 6 "net/url" 7 "time" 8 9 "github.com/cozy/cozy-stack/model/instance" 10 "github.com/cozy/cozy-stack/model/job" 11 "github.com/cozy/cozy-stack/model/permission" 12 "github.com/cozy/cozy-stack/pkg/appfs" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/pkg/metadata" 16 "github.com/cozy/cozy-stack/pkg/prefixer" 17 ) 18 19 // KonnManifest contains all the informations associated with an installed 20 // konnector. 21 type KonnManifest struct { 22 doc *couchdb.JSONDoc 23 err error 24 25 val struct { 26 // Fields that can be read and updated 27 Slug string `json:"slug"` 28 Source string `json:"source"` 29 State State `json:"state"` 30 Version string `json:"version"` 31 AvailableVersion string `json:"available_version"` 32 Checksum string `json:"checksum"` 33 Parameters map[string]interface{} `json:"parameters"` 34 CreatedAt time.Time `json:"created_at"` 35 UpdatedAt time.Time `json:"updated_at"` 36 Err string `json:"error"` 37 38 // Just readers 39 Name string `json:"name"` 40 Icon string `json:"icon"` 41 Language string `json:"language"` 42 ClientSide bool `json:"clientSide"` 43 OnDeleteAccount string `json:"on_delete_account"` 44 45 // Fields with complex types 46 Permissions permission.Set `json:"permissions"` 47 Terms Terms `json:"terms"` 48 Notifications Notifications `json:"notifications"` 49 } 50 } 51 52 // ID is part of the Manifest interface 53 func (m *KonnManifest) ID() string { return m.doc.ID() } 54 55 // Rev is part of the Manifest interface 56 func (m *KonnManifest) Rev() string { return m.doc.Rev() } 57 58 // DocType is part of the Manifest interface 59 func (m *KonnManifest) DocType() string { return consts.Konnectors } 60 61 // Clone is part of the Manifest interface 62 func (m *KonnManifest) Clone() couchdb.Doc { 63 cloned := *m 64 cloned.doc = m.doc.Clone().(*couchdb.JSONDoc) 65 cloned.val.Permissions = make(permission.Set, len(m.val.Permissions)) 66 copy(cloned.val.Permissions, m.val.Permissions) 67 return &cloned 68 } 69 70 // SetID is part of the Manifest interface 71 func (m *KonnManifest) SetID(id string) { m.doc.SetID(id) } 72 73 // SetRev is part of the Manifest interface 74 func (m *KonnManifest) SetRev(rev string) { m.doc.SetRev(rev) } 75 76 // SetSlug is part of the Manifest interface 77 func (m *KonnManifest) SetSlug(slug string) { m.val.Slug = slug } 78 79 // SetSource is part of the Manifest interface 80 func (m *KonnManifest) SetSource(src *url.URL) { m.val.Source = src.String() } 81 82 // Source is part of the Manifest interface 83 func (m *KonnManifest) Source() string { return m.val.Source } 84 85 // Version is part of the Manifest interface 86 func (m *KonnManifest) Version() string { return m.val.Version } 87 88 // AvailableVersion is part of the Manifest interface 89 func (m *KonnManifest) AvailableVersion() string { return m.val.AvailableVersion } 90 91 // Checksum is part of the Manifest interface 92 func (m *KonnManifest) Checksum() string { return m.val.Checksum } 93 94 // Slug is part of the Manifest interface 95 func (m *KonnManifest) Slug() string { return m.val.Slug } 96 97 // State is part of the Manifest interface 98 func (m *KonnManifest) State() State { return m.val.State } 99 100 // LastUpdate is part of the Manifest interface 101 func (m *KonnManifest) LastUpdate() time.Time { return m.val.UpdatedAt } 102 103 // SetState is part of the Manifest interface 104 func (m *KonnManifest) SetState(state State) { m.val.State = state } 105 106 // SetVersion is part of the Manifest interface 107 func (m *KonnManifest) SetVersion(version string) { m.val.Version = version } 108 109 // SetAvailableVersion is part of the Manifest interface 110 func (m *KonnManifest) SetAvailableVersion(version string) { m.val.AvailableVersion = version } 111 112 // SetChecksum is part of the Manifest interface 113 func (m *KonnManifest) SetChecksum(shasum string) { m.val.Checksum = shasum } 114 115 // AppType is part of the Manifest interface 116 func (m *KonnManifest) AppType() consts.AppType { return consts.KonnectorType } 117 118 // Terms is part of the Manifest interface 119 func (m *KonnManifest) Terms() Terms { return m.val.Terms } 120 121 // Permissions is part of the Manifest interface 122 func (m *KonnManifest) Permissions() permission.Set { return m.val.Permissions } 123 124 // SetError is part of the Manifest interface 125 func (m *KonnManifest) SetError(err error) { 126 m.SetState(Errored) 127 m.val.Err = err.Error() 128 m.err = err 129 } 130 131 // Error is part of the Manifest interface 132 func (m *KonnManifest) Error() error { return m.err } 133 134 // Fetch is part of the Manifest interface 135 func (m *KonnManifest) Fetch(field string) []string { 136 switch field { 137 case "slug": 138 return []string{m.val.Slug} 139 case "state": 140 return []string{string(m.val.State)} 141 } 142 return nil 143 } 144 145 // Notifications returns the notifications properties for this konnector. 146 func (m *KonnManifest) Notifications() Notifications { 147 return m.val.Notifications 148 } 149 150 // Parameters returns the parameters for executing the konnector. 151 func (m *KonnManifest) Parameters() map[string]interface{} { 152 return m.val.Parameters 153 } 154 155 // Name returns the konnector name. 156 func (m *KonnManifest) Name() string { return m.val.Name } 157 158 // Icon returns the konnector icon path. 159 func (m *KonnManifest) Icon() string { return m.val.Icon } 160 161 // Language returns the programming language used for executing the konnector 162 // (only "node" for the moment). 163 func (m *KonnManifest) Language() string { return m.val.Language } 164 165 // ClientSide returns true for a konnector that runs on the client (flagship 166 // app), and false for a konnector that runs on the server (nodejs executed by 167 // the stack). 168 func (m *KonnManifest) ClientSide() bool { return m.val.ClientSide } 169 170 // OnDeleteAccount can be used to specify a file path which will be executed 171 // when an account associated with the konnector is deleted. 172 func (m *KonnManifest) OnDeleteAccount() string { return m.val.OnDeleteAccount } 173 174 // VendorLink returns the vendor link. 175 func (m *KonnManifest) VendorLink() interface{} { 176 return m.doc.M["vendor_link"] 177 } 178 179 func (m *KonnManifest) MarshalJSON() ([]byte, error) { 180 doc := m.doc.Clone().(*couchdb.JSONDoc) 181 doc.Type = consts.Konnectors 182 doc.M["slug"] = m.val.Slug 183 doc.M["source"] = m.val.Source 184 doc.M["state"] = m.val.State 185 doc.M["version"] = m.val.Version 186 if m.val.AvailableVersion == "" { 187 delete(doc.M, "available_version") 188 } else { 189 doc.M["available_version"] = m.val.AvailableVersion 190 } 191 doc.M["checksum"] = m.val.Checksum 192 if m.val.Parameters == nil { 193 delete(doc.M, "parameters") 194 } else { 195 doc.M["parameters"] = m.val.Parameters 196 } 197 doc.M["created_at"] = m.val.CreatedAt 198 doc.M["updated_at"] = m.val.UpdatedAt 199 if m.val.Err == "" { 200 delete(doc.M, "error") 201 } else { 202 doc.M["error"] = m.val.Err 203 } 204 // XXX: keep the weird UnmarshalJSON of permission.Set 205 perms, err := m.val.Permissions.MarshalJSON() 206 if err != nil { 207 return nil, err 208 } 209 doc.M["permissions"] = json.RawMessage(perms) 210 return json.Marshal(doc) 211 } 212 213 func (m *KonnManifest) UnmarshalJSON(j []byte) error { 214 if err := json.Unmarshal(j, &m.doc); err != nil { 215 return err 216 } 217 if err := json.Unmarshal(j, &m.val); err != nil { 218 return err 219 } 220 return nil 221 } 222 223 // ReadManifest is part of the Manifest interface 224 func (m *KonnManifest) ReadManifest(r io.Reader, slug, sourceURL string) (Manifest, error) { 225 var newManifest KonnManifest 226 if err := json.NewDecoder(r).Decode(&newManifest); err != nil { 227 return nil, ErrBadManifest 228 } 229 230 newManifest.SetID(consts.Konnectors + "/" + slug) 231 newManifest.SetRev(m.Rev()) 232 newManifest.SetState(m.State()) 233 newManifest.val.CreatedAt = m.val.CreatedAt 234 newManifest.val.Slug = slug 235 newManifest.val.Source = sourceURL 236 if newManifest.val.Parameters == nil { 237 newManifest.val.Parameters = m.val.Parameters 238 } 239 240 return &newManifest, nil 241 } 242 243 // Create is part of the Manifest interface 244 func (m *KonnManifest) Create(db prefixer.Prefixer) error { 245 m.SetID(consts.Konnectors + "/" + m.Slug()) 246 m.val.CreatedAt = time.Now() 247 m.val.UpdatedAt = time.Now() 248 if err := couchdb.CreateNamedDocWithDB(db, m); err != nil { 249 return err 250 } 251 252 _, err := permission.CreateKonnectorSet(db, m.Slug(), m.Permissions(), m.Version()) 253 return err 254 } 255 256 // Update is part of the Manifest interface 257 func (m *KonnManifest) Update(db prefixer.Prefixer, extraPerms permission.Set) error { 258 m.val.UpdatedAt = time.Now() 259 err := couchdb.UpdateDoc(db, m) 260 if err != nil { 261 return err 262 } 263 264 perms := m.Permissions() 265 266 // Merging the potential extra permissions 267 if len(extraPerms) > 0 { 268 perms, err = permission.MergeExtraPermissions(perms, extraPerms) 269 if err != nil { 270 return err 271 } 272 } 273 _, err = permission.UpdateKonnectorSet(db, m.Slug(), perms) 274 return err 275 } 276 277 // Delete is part of the Manifest interface 278 func (m *KonnManifest) Delete(db prefixer.Prefixer) error { 279 err := permission.DestroyKonnector(db, m.Slug()) 280 if err != nil && !couchdb.IsNotFoundError(err) { 281 return err 282 } 283 return couchdb.DeleteDoc(db, m) 284 } 285 286 // BuildTrigger builds a @cron trigger with the parameter from the konnector 287 // manifest (not yet persisted in CouchDB). 288 func (m *KonnManifest) BuildTrigger(db prefixer.Prefixer, accountID, createdByApp string) (job.Trigger, error) { 289 var md *metadata.CozyMetadata 290 if createdByApp == "" { 291 md = metadata.New() 292 } else { 293 var err error 294 md, err = metadata.NewWithApp(createdByApp, "", job.DocTypeVersionTrigger) 295 if err != nil { 296 return nil, err 297 } 298 } 299 md.DocTypeVersion = "1" 300 data := map[string]interface{}{ 301 "account": accountID, 302 "konnector": m.Slug(), 303 } 304 if m.hasFolderPath() { 305 // XXX in theory, it is an ID, but we just put the yes string and let 306 // the worker change it to the folder ID on the first run. 307 data["folder_to_save"] = "yes" 308 } 309 msg, err := job.NewMessage(data) 310 if err != nil { 311 return nil, err 312 } 313 crontab := m.triggerCrontab() 314 return job.NewCronTrigger(&job.TriggerInfos{ 315 Type: "@cron", 316 WorkerType: "konnector", 317 Domain: db.DomainName(), 318 Prefix: db.DBPrefix(), 319 Arguments: crontab, 320 Message: msg, 321 Metadata: md, 322 }) 323 } 324 325 // CreateTrigger creates a @cron trigger with the parameter from the konnector 326 // manifest (persisted in CouchDB). 327 func (m *KonnManifest) CreateTrigger(db prefixer.Prefixer, accountID, createdByApp string) (job.Trigger, error) { 328 t, err := m.BuildTrigger(db, accountID, createdByApp) 329 if err != nil { 330 return nil, err 331 } 332 sched := job.System() 333 if err = sched.AddTrigger(t); err != nil { 334 return nil, err 335 } 336 return t, nil 337 } 338 339 func (m *KonnManifest) triggerCrontab() string { 340 spec := job.NewPeriodicSpec() 341 342 freq, _ := m.doc.M["frequency"].(string) 343 switch freq { 344 case "hourly": 345 spec.Frequency = job.HourlyKind 346 case "daily": 347 spec.Frequency = job.DailyKind 348 case "monthly": 349 spec.Frequency = job.MonthlyKind 350 default: // weekly 351 spec.Frequency = job.WeeklyKind 352 } 353 354 min, max := 0, 5 // By default konnectors are run at random hour between 12:00PM and 05:00AM 355 interval, ok := m.doc.M["time_interval"].([]int) 356 if ok && len(interval) == 2 { 357 min = interval[0] 358 if interval[1] > min { 359 max = interval[1] 360 } 361 } 362 spec.AfterHour = min 363 spec.BeforeHour = max 364 365 return spec.ToRandomCrontab(m.Slug()) 366 } 367 368 // Cf https://github.com/cozy/cozy-libs/blob/55b5f23f0adbc308c3b70fa287c3938ee1b0a4cc/packages/cozy-harvest-lib/src/helpers/konnectors.js#L213-L225 369 func (m *KonnManifest) hasFolderPath() bool { 370 if _, ok := m.doc.M["folders"].([]interface{}); ok { 371 return true 372 } 373 fields, ok := m.doc.M["fields"].(map[string]interface{}) 374 if !ok { 375 return false 376 } 377 advanced, ok := fields["advanced_fields"].(map[string]interface{}) 378 if !ok { 379 return false 380 } 381 return advanced["folderPath"] != nil 382 } 383 384 // GetKonnectorBySlug fetch the manifest of a konnector from the database given 385 // a slug. 386 func GetKonnectorBySlug(db prefixer.Prefixer, slug string) (*KonnManifest, error) { 387 if slug == "" || !slugReg.MatchString(slug) { 388 return nil, ErrInvalidSlugName 389 } 390 doc := &KonnManifest{} 391 err := couchdb.GetDoc(db, consts.Konnectors, consts.Konnectors+"/"+slug, doc) 392 if couchdb.IsNotFoundError(err) { 393 return nil, ErrNotFound 394 } 395 if err != nil { 396 return nil, err 397 } 398 return doc, nil 399 } 400 401 // GetKonnectorBySlugAndUpdate fetch the KonnManifest and perform an update of 402 // the konnector if necessary and if the konnector was installed from the 403 // registry. 404 func GetKonnectorBySlugAndUpdate(in *instance.Instance, slug string, copier appfs.Copier, registries []*url.URL) (*KonnManifest, error) { 405 man, err := GetKonnectorBySlug(in, slug) 406 if err != nil { 407 return nil, err 408 } 409 return DoLazyUpdate(in, man, copier, registries).(*KonnManifest), nil 410 } 411 412 // ListKonnectorsWithPagination returns the list of installed konnectors with a 413 // pagination 414 func ListKonnectorsWithPagination(db prefixer.Prefixer, limit int, startKey string) ([]*KonnManifest, string, error) { 415 var docs []*KonnManifest 416 417 if limit == 0 { 418 limit = defaultAppListLimit 419 } 420 421 req := &couchdb.AllDocsRequest{ 422 Limit: limit + 1, // Also get the following document for the next key 423 StartKey: startKey, 424 } 425 err := couchdb.GetAllDocs(db, consts.Konnectors, req, &docs) 426 if err != nil { 427 return nil, "", err 428 } 429 430 nextID := "" 431 if len(docs) > 0 && len(docs) == limit+1 { // There are still documents to fetch 432 nextDoc := docs[len(docs)-1] 433 nextID = nextDoc.ID() 434 docs = docs[:len(docs)-1] 435 } 436 437 return docs, nextID, nil 438 } 439 440 var _ Manifest = &KonnManifest{}