github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/directeurs/lettre.go (about) 1 package directeurs 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "mime" 8 "mime/multipart" 9 "net/url" 10 "path/filepath" 11 "strings" 12 "time" 13 14 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 15 "github.com/benoitkugler/goACVE/server/core/utils/pdf" 16 "github.com/benoitkugler/goACVE/server/documents" 17 "github.com/benoitkugler/goACVE/server/shared" 18 "github.com/labstack/echo" 19 "github.com/lib/pq" 20 "golang.org/x/net/html" 21 "golang.org/x/net/html/atom" 22 ) 23 24 const ( 25 MAX_IMAGE_SIZE = 1000000 // bytes 26 PATH_LOAD_IMAGES_LETTRE = "/api/imageslettre" 27 ) 28 29 func GenerateNewImageLien(db rd.DB) (string, error) { 30 newLien := rd.RandString(64, false) 31 for { 32 row := db.QueryRow("SELECT count(*) FROM imageuploadeds WHERE lien = $1", newLien) 33 var count int 34 if err := row.Scan(&count); err != nil { 35 return "", err 36 } 37 if count == 0 { // OK, le lien est libre 38 break 39 } 40 newLien = rd.RandString(64, false) // déjà pris, on ré-essaye 41 } 42 return newLien, nil 43 } 44 45 func GetImagePublicURL(host, lien string) string { 46 return shared.BuildUrl(host, PATH_LOAD_IMAGES_LETTRE, map[string]string{"lien": lien}) 47 } 48 49 func (ct Controller) uploadImageLettre(fileHeader *multipart.FileHeader, host string, idCamp int64) (string, error) { 50 filename, content, err := documents.ReceiveUpload(fileHeader, MAX_IMAGE_SIZE) 51 if err != nil { 52 return "", err 53 } 54 55 lien, err := GenerateNewImageLien(ct.DB) 56 if err != nil { 57 return "", err 58 } 59 60 tx, err := ct.DB.Begin() 61 if err != nil { 62 return "", err 63 } 64 65 img := rd.Imageuploaded{IdCamp: idCamp, Filename: filename, Content: content, Lien: lien} 66 err = rd.InsertManyImageuploadeds(tx, img) 67 if err != nil { 68 return "", shared.Rollback(tx, err) 69 } 70 err = tx.Commit() 71 72 location := GetImagePublicURL(host, lien) 73 74 return location, err 75 } 76 77 type urlAuth struct { 78 idCamp, token string 79 } 80 81 func (u urlAuth) BasicAuth() (username, password string, ok bool) { 82 return u.idCamp, u.token, true 83 } 84 85 // Location of the image as expected by Tinymce 86 type UploadImageLettreOut struct { 87 Location string `json:"location"` 88 } 89 90 // UploadImageLettre enregistre une image de la lettre au directeur 91 // et renvoie un chemin d'accès. 92 func (ct Controller) UploadImageLettre(c echo.Context) error { 93 idCampS, token := c.Param("id_camp"), c.Param("token") 94 idCamp, _, err := ct.authentifie(urlAuth{idCamp: idCampS, token: token}) 95 if err != nil { 96 return err 97 } 98 host := c.Request().Host 99 fileHeader, err := c.FormFile("file") 100 if err != nil { 101 return err 102 } 103 location, err := ct.uploadImageLettre(fileHeader, host, idCamp) 104 if err != nil { 105 return err 106 } 107 out := UploadImageLettreOut{Location: location} 108 return c.JSON(200, out) 109 } 110 111 // LoadImageLettre renvoie l'image demandée. La sécurité est assurée 112 // par la complexité du lien 113 func (ct Controller) LoadImageLettre(c echo.Context) error { 114 lien := c.QueryParam("lien") 115 // dans le cas ou plusieurs images ont le même lien (copie d'une lettre d'une année sur l'autre) 116 // une seule a besoin d'être renvoyée : on limite 117 row := ct.DB.QueryRow("SELECT * FROM imageuploadeds WHERE lien = $1 LIMIT 1", lien) 118 img, err := rd.ScanImageuploaded(row) 119 if err != nil { 120 return err 121 } 122 return c.Blob(200, mime.TypeByExtension(filepath.Ext(img.Filename)), img.Content) 123 } 124 125 // updateLettreDirecteur enregistre le html et génère et enregistre le document associé. 126 // S'il existe, le document est remplacé. 127 // La mise à jour de la lettre se fait en 6 étapes : 128 // 0. mise à jour des paramètres (html + options) 129 // 1. la lecture ou la création des méta-données du document final (Document) 130 // 2. la génération du Pdf, via weasyprint 131 // 3. l'enregistrement du Pdf comme contenu du document 132 // 4. le partage du document final 133 // 5. le nettoyage (en arrière plan) des images non utilisées 134 // Dans le cas où la lettre n'a pas été modifié, la seule étape nécessaire est l'étape 4 135 func (d DriverCampComplet) updateLettreDirecteur(host string, lettre rd.Lettredirecteur) (pub documents.PublicDocument, err error) { 136 var ( 137 pdfContent []byte 138 dirPers rd.Personne 139 fileName string 140 ) 141 142 tx, err := d.DB.Begin() 143 if err != nil { 144 return 145 } 146 147 // Etape 0: on prépare les paramètres de la lettre 148 lettre.IdCamp = d.camp.Id 149 lettre.Html = sanitizeHtml(lettre.Html) // sécurité + problèmes weasyprint 150 151 // Etape 2 152 docs, err := rd.SelectDocumentCampsByIdCamps(tx, d.camp.Id) 153 if err != nil { 154 err = shared.Rollback(tx, err) 155 return 156 } 157 lienDoc, hasFinalDocument := docs.FindLettre() 158 var docLettre rd.Document 159 if hasFinalDocument { // on récupère le document final déjà existant 160 docLettre, err = rd.SelectDocument(tx, lienDoc.IdDocument) 161 if err != nil { 162 err = shared.Rollback(tx, err) 163 return 164 } 165 } else { // pas encore de doc associé, on crée 166 docLettre = rd.Document{ 167 Description: "Généré à partir de la lettre du directeur.", 168 DateHeureModif: rd.Time(time.Now()), 169 } 170 docLettre, err = docLettre.Insert(tx) 171 if err != nil { 172 err = shared.Rollback(tx, err) 173 return 174 } 175 lienDoc = rd.DocumentCamp{IdCamp: d.Camp().Id, IdDocument: docLettre.Id, IsLettre: true} 176 err = rd.InsertManyDocumentCamps(tx, lienDoc) 177 if err != nil { 178 err = shared.Rollback(tx, err) 179 return 180 } 181 } 182 183 // Pour optimiser l'affichage, on commence par vérifier si le contenu a changé 184 // (et si il y a déjà un document final enregistré) 185 currentLettre, hasCurrentLettre, err := rd.SelectLettredirecteurByIdCamp(tx, d.camp.Id) 186 if err != nil { 187 err = shared.Rollback(tx, err) 188 return 189 } 190 if hasCurrentLettre && currentLettre == lettre && hasFinalDocument { 191 // la génération et l'enregistrement n'est pas nécessaire 192 goto Publie 193 } 194 195 // on efface un doublon potentiel 196 err = lettre.Delete(tx) 197 if err != nil { 198 err = shared.Rollback(tx, err) 199 return 200 } 201 // et on insère 202 err = rd.InsertManyLettredirecteurs(tx, lettre) 203 if err != nil { 204 err = shared.Rollback(tx, err) 205 return 206 } 207 208 // Etape 2 209 if !lettre.UseCoordCentre { 210 eqs, err := rd.SelectEquipiersByIdCamps(tx, d.camp.Id) 211 if err != nil { 212 err = shared.Rollback(tx, err) 213 return pub, err 214 } 215 dir, has := eqs.FindDirecteur() 216 if !has { 217 err = shared.Rollback(tx, errors.New("Aucun directeur n'est déclaré pour ce camp !")) 218 return pub, err 219 } 220 dirPers, err = rd.SelectPersonne(tx, dir.IdPersonne) 221 if err != nil { 222 err = shared.Rollback(tx, err) 223 return pub, err 224 } 225 } 226 227 pdfContent, err = pdf.LettreDirecteur(lettre, dirPers) 228 if err != nil { 229 err = shared.Rollback(tx, err) 230 return 231 } 232 233 // Etape 3 234 fileName = fmt.Sprintf("Lettre du directeur %s.pdf", reduitNom(d.camp.RawData().Label().String())) 235 docLettre, err = documents.SaveDocument(tx, docLettre.Id, pdfContent, fileName, false) 236 if err != nil { 237 err = shared.Rollback(tx, err) 238 return 239 } 240 241 Publie: 242 243 // Etape4 244 pub, err = documents.PublieDocument(d.Signing, host, docLettre) 245 if err != nil { 246 err = shared.Rollback(tx, err) 247 return 248 } 249 250 // Etape 5 251 err = tx.Commit() 252 if err == nil { 253 // tout est OK, on peut lancer le nettoyage en tache de fond 254 go d.garbageCollectImages(lettre.Html) 255 } 256 257 return pub, err 258 } 259 260 func extractImagesLiens(htmlLettre string) []string { 261 node, err := html.Parse(strings.NewReader(htmlLettre)) 262 if err != nil { 263 log.Println("error extracting images", err) 264 return nil 265 } 266 var ( 267 f func(n *html.Node) 268 out []string 269 ) 270 f = func(n *html.Node) { 271 if n.Type == html.ElementNode && n.DataAtom == atom.Img { 272 for _, attr := range n.Attr { 273 if attr.Key == "src" { // found an image 274 parsed, err := url.Parse(attr.Val) 275 if err != nil { 276 log.Println("error parsing image url", err) 277 continue 278 } 279 lien := parsed.Query().Get("lien") 280 out = append(out, lien) 281 } 282 } 283 } 284 for c := n.FirstChild; c != nil; c = c.NextSibling { 285 f(c) 286 } 287 } 288 f(node) 289 return out 290 } 291 292 func (d DriverCampComplet) garbageCollectImages(htmlLettre string) { 293 usedLiens := extractImagesLiens(htmlLettre) 294 // on supprime les images non utilisées, uniquement pour le séjour courant 295 // car un même lien peut-être partagé entre 2 séjours 296 res, err := d.DB.Exec("DELETE FROM imageuploadeds WHERE id_camp = $1 AND NOT (lien = ANY($2))", d.camp.Id, pq.StringArray(usedLiens)) 297 if err != nil { 298 log.Println("error removing unsued images", err) 299 return 300 } 301 if rowsNb, _ := res.RowsAffected(); rowsNb > 0 { 302 log.Printf("camp %d -> removed %d unused images\n", d.camp.Id, rowsNb) 303 } 304 }