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  }