github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/cmd/fix.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/mail" 9 "net/url" 10 "os" 11 "regexp" 12 "strings" 13 14 "github.com/cozy/cozy-stack/client" 15 "github.com/cozy/cozy-stack/client/request" 16 "github.com/cozy/cozy-stack/model/contact" 17 "github.com/cozy/cozy-stack/model/vfs" 18 "github.com/cozy/cozy-stack/pkg/consts" 19 20 "github.com/spf13/cobra" 21 ) 22 23 var ( 24 dryRunFlag bool 25 withMetadataFlag bool 26 ) 27 28 var fixerCmdGroup = &cobra.Command{ 29 Use: "fix <command>", 30 Aliases: []string{"fixer"}, 31 Short: "A set of tools to fix issues or migrate content.", 32 RunE: func(cmd *cobra.Command, args []string) error { 33 return cmd.Usage() 34 }, 35 } 36 37 var mimeFixerCmd = &cobra.Command{ 38 Use: "mime <domain>", 39 Short: "Fix the class computed from the mime-type", 40 RunE: func(cmd *cobra.Command, args []string) error { 41 if len(args) == 0 { 42 return cmd.Usage() 43 } 44 c := newClient(args[0], consts.Files) 45 return c.WalkByPath("/", func(name string, doc *client.DirOrFile, err error) error { 46 if err != nil { 47 return err 48 } 49 attrs := doc.Attrs 50 if attrs.Type == consts.DirType { 51 return nil 52 } 53 _, class := vfs.ExtractMimeAndClassFromFilename(attrs.Name) 54 if class == attrs.Class { 55 return nil 56 } 57 fmt.Fprintf(os.Stdout, "Fix %s: %s -> %s\n", attrs.Name, attrs.Class, class) 58 _, err = c.UpdateAttrsByID(doc.ID, &client.FilePatch{ 59 Rev: doc.Rev, 60 Attrs: client.FilePatchAttrs{ 61 Class: class, 62 }, 63 }) 64 return err 65 }) 66 }, 67 } 68 69 var jobsFixer = &cobra.Command{ 70 Use: "jobs <domain>", 71 Short: "Take a look at the consistency of the jobs", 72 RunE: func(cmd *cobra.Command, args []string) error { 73 if len(args) == 0 { 74 return cmd.Usage() 75 } 76 c := newClient(args[0], consts.Jobs) 77 res, err := c.Req(&request.Options{ 78 Method: "POST", 79 Path: "/jobs/clean", 80 }) 81 if err != nil { 82 return err 83 } 84 defer res.Body.Close() 85 var result struct { 86 Deleted int `json:"deleted"` 87 } 88 err = json.NewDecoder(res.Body).Decode(&result) 89 if err != nil { 90 return err 91 } 92 93 fmt.Fprintf(os.Stdout, "Cleaned %d jobs on %s\n", result.Deleted, args[0]) 94 return nil 95 }, 96 } 97 98 var redisFixer = &cobra.Command{ 99 Use: "redis", 100 Short: "Rebuild scheduling data strucutures in redis", 101 RunE: func(cmd *cobra.Command, args []string) error { 102 ac := newAdminClient() 103 return ac.RebuildRedis() 104 }, 105 } 106 107 var thumbnailsFixer = &cobra.Command{ 108 Use: "thumbnails <domain>", 109 Short: "Rebuild thumbnails image for images files", 110 RunE: func(cmd *cobra.Command, args []string) error { 111 if len(args) != 1 { 112 return cmd.Usage() 113 } 114 domain := args[0] 115 c := newClient(domain, "io.cozy.jobs") 116 res, err := c.JobPush(&client.JobOptions{ 117 Worker: "thumbnailck", 118 Arguments: struct { 119 WithMetadata bool `json:"with_metadata"` 120 }{ 121 WithMetadata: withMetadataFlag, 122 }, 123 }) 124 if err != nil { 125 return err 126 } 127 b, err := json.MarshalIndent(res, "", " ") 128 if err != nil { 129 return err 130 } 131 fmt.Println(string(b)) 132 return nil 133 }, 134 } 135 136 var contactEmailsFixer = &cobra.Command{ 137 Use: "contact-emails", 138 Short: "Detect and try to fix invalid emails on contacts", 139 RunE: func(cmd *cobra.Command, args []string) error { 140 ac := newAdminClient() 141 instances, err := ac.ListInstances() 142 if err != nil { 143 return err 144 } 145 146 semicolonAtEnd := regexp.MustCompile(";$") 147 dotAtStart := regexp.MustCompile(`^\.`) 148 dotAtEnd := regexp.MustCompile(`\.$`) 149 lessThanStart := regexp.MustCompile("^<") 150 greaterThanEnd := regexp.MustCompile(">$") 151 mailto := regexp.MustCompile("^mailto:") 152 153 fixEmails := func(domain string) error { 154 c, err := newClientSafe(domain, consts.Contacts) 155 if err != nil { 156 return err 157 } 158 159 res, err := c.Req(&request.Options{ 160 Method: "GET", 161 Path: "/data/" + consts.Contacts + "/_all_docs", 162 Queries: url.Values{ 163 "include_docs": {"true"}, 164 }, 165 }) 166 if err != nil { 167 return err 168 } 169 defer res.Body.Close() 170 171 var contacts struct { 172 Rows []struct { 173 Contact contact.Contact `json:"doc"` 174 } `json:"rows"` 175 } 176 buf, err := io.ReadAll(res.Body) 177 if err != nil { 178 return err 179 } 180 err = json.Unmarshal(buf, &contacts) 181 if err != nil { 182 return err 183 } 184 185 for _, r := range contacts.Rows { 186 co := r.Contact 187 id := co.ID() 188 if strings.HasPrefix(id, "_design") { 189 continue 190 } 191 192 changed := false 193 emails, ok := co.Get("emails").([]interface{}) 194 if !ok { 195 continue 196 } 197 for i := range emails { 198 email, ok := emails[i].(map[string]interface{}) 199 if !ok { 200 continue 201 } 202 address, ok := email["address"].(string) 203 if !ok { 204 continue 205 } 206 _, err := mail.ParseAddress(address) 207 if err != nil { 208 old := address 209 address = strings.TrimSpace(address) 210 address = strings.ReplaceAll(address, "\"", "") 211 address = strings.ReplaceAll(address, ",", ".") 212 address = strings.ReplaceAll(address, " .", ".") 213 address = strings.ReplaceAll(address, ". ", ".") 214 address = strings.ReplaceAll(address, ". ", ".") 215 address = strings.ReplaceAll(address, "..", ".") 216 address = strings.ReplaceAll(address, ".@", "@") 217 address = strings.ReplaceAll(address, "@.", "@") 218 address = strings.ReplaceAll(address, " @", "@") 219 address = strings.ReplaceAll(address, "@ ", "@") 220 address = mailto.ReplaceAllString(address, "") 221 address = semicolonAtEnd.ReplaceAllString(address, "") 222 address = dotAtStart.ReplaceAllString(address, "") 223 address = dotAtEnd.ReplaceAllString(address, "") 224 address = lessThanStart.ReplaceAllString(address, "") 225 address = greaterThanEnd.ReplaceAllString(address, "") 226 address = strings.TrimSpace(address) 227 _, err := mail.ParseAddress(address) 228 if err == nil { 229 fmt.Fprintf(os.Stdout, " Email fixed: \"%s\" → \"%s\"\n", old, address) 230 changed = true 231 email["address"] = address 232 } else { 233 fmt.Fprintf(os.Stdout, " Invalid email: \"%s\" → \"%s\"\n", old, address) 234 } 235 } 236 } 237 238 if changed { 239 co.M["email"] = emails 240 json, err := json.Marshal(co) 241 if err != nil { 242 return err 243 } 244 body := bytes.NewReader(json) 245 246 _, err = c.Req(&request.Options{ 247 Method: "PUT", 248 Path: "/data/" + consts.Contacts + "/" + id, 249 Body: body, 250 }) 251 if err != nil { 252 return err 253 } 254 } 255 } 256 257 return nil 258 } 259 260 for _, instance := range instances { 261 domain := instance.Attrs.Domain 262 fmt.Fprintf(os.Stderr, "Fixing %s contact emails...\n", domain) 263 err := fixEmails(domain) 264 if err != nil { 265 fmt.Fprintf(os.Stderr, "Error occurred: %s\n", err) 266 } 267 } 268 269 return nil 270 }, 271 } 272 273 var passwordDefinedFixer = &cobra.Command{ 274 Use: "password-defined <domain>", 275 Short: "Set the password_defined setting", 276 Long: ` 277 A password_defined field has been added to the io.cozy.settings.instance 278 (available on GET /settings/instance). This fixer can fill it for existing Cozy 279 instances if it was missing. 280 `, 281 RunE: func(cmd *cobra.Command, args []string) error { 282 if len(args) != 1 { 283 return cmd.Usage() 284 } 285 domain := args[0] 286 c := newAdminClient() 287 path := fmt.Sprintf("/instances/%s/fixers/password-defined", domain) 288 _, err := c.Req(&request.Options{ 289 Method: "POST", 290 Path: path, 291 }) 292 return err 293 }, 294 } 295 296 var orphanAccountFixer = &cobra.Command{ 297 Use: "orphan-account <domain>", 298 Short: "Remove the orphan accounts", 299 Long: ` 300 This fixer detects the accounts that are linked to a konnector that has been 301 uninstalled, and then removed them. 302 303 For banking accounts, the konnector must run to also clean the account 304 remotely. To do so, the konnector is installed, the account is deleted, 305 the stack runs the konnector with the AccountDeleted flag, and when it's 306 done, the konnector is uninstalled again. 307 `, 308 RunE: func(cmd *cobra.Command, args []string) error { 309 if len(args) != 1 { 310 return cmd.Usage() 311 } 312 domain := args[0] 313 c := newAdminClient() 314 path := fmt.Sprintf("/instances/%s/fixers/orphan-account", domain) 315 _, err := c.Req(&request.Options{ 316 Method: "POST", 317 Path: path, 318 }) 319 return err 320 }, 321 } 322 323 var serviceTriggersFixer = &cobra.Command{ 324 Use: "service-triggers <domain>", 325 Short: "Clean the triggers for webapp services", 326 Long: ` 327 This fixer cleans duplicate triggers for webapp services, and recreates missing 328 triggers. 329 `, 330 RunE: func(cmd *cobra.Command, args []string) error { 331 if len(args) != 1 { 332 return cmd.Usage() 333 } 334 domain := args[0] 335 c := newAdminClient() 336 path := fmt.Sprintf("/instances/%s/fixers/service-triggers", domain) 337 res, err := c.Req(&request.Options{ 338 Method: "POST", 339 Path: path, 340 }) 341 if err != nil { 342 return err 343 } 344 out, err := io.ReadAll(res.Body) 345 if err != nil { 346 return err 347 } 348 fmt.Print(string(out)) 349 return nil 350 }, 351 } 352 353 var indexesFixer = &cobra.Command{ 354 Use: "indexes <domain>", 355 Short: "Rebuild the CouchDB views and indexes", 356 Long: ` 357 This fixer ensures that the CouchDB views and indexes used by the stack for 358 this instance are correctly set. 359 `, 360 RunE: func(cmd *cobra.Command, args []string) error { 361 if len(args) != 1 { 362 return cmd.Usage() 363 } 364 domain := args[0] 365 c := newAdminClient() 366 path := fmt.Sprintf("/instances/%s/fixers/indexes", domain) 367 _, err := c.Req(&request.Options{ 368 Method: "POST", 369 Path: path, 370 }) 371 return err 372 }, 373 } 374 375 func init() { 376 thumbnailsFixer.Flags().BoolVar(&dryRunFlag, "dry-run", false, "Dry run") 377 thumbnailsFixer.Flags().BoolVar(&withMetadataFlag, "with-metadata", false, "Recalculate images metadata") 378 379 fixerCmdGroup.AddCommand(jobsFixer) 380 fixerCmdGroup.AddCommand(mimeFixerCmd) 381 fixerCmdGroup.AddCommand(redisFixer) 382 fixerCmdGroup.AddCommand(thumbnailsFixer) 383 fixerCmdGroup.AddCommand(contactEmailsFixer) 384 fixerCmdGroup.AddCommand(passwordDefinedFixer) 385 fixerCmdGroup.AddCommand(orphanAccountFixer) 386 fixerCmdGroup.AddCommand(serviceTriggersFixer) 387 fixerCmdGroup.AddCommand(indexesFixer) 388 389 RootCmd.AddCommand(fixerCmdGroup) 390 }