package tools import ( "encoding/json" "fmt" "github.com/xuri/excelize/v2" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "io/ioutil" "net/http" "net/url" "path/filepath" "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model" "regexp" "sort" "strconv" "strings" "sync" ) const ( bucketImages = "squizimages" bucketFonts = "squizfonts" bucketScripts = "squizscript" bucketStyle = "squizstyle" bucketAnswers = "squizanswer" ) func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer, s3Prefix string) error { file := excelize.NewFile() sheet := "Sheet1" _, err := file.NewSheet(sheet) if err != nil { return err } sort.Slice(questions, func(i, j int) bool { return questions[i].Page < questions[j].Page }) headers, mapQueRes := prepareHeaders(questions) for col, header := range headers { cell := ToAlphaString(col+1) + "1" if err := file.SetCellValue(sheet, cell, header); err != nil { return err } } sort.Slice(answers, func(i, j int) bool { return answers[i].QuestionId < answers[j].QuestionId }) standart, results := categorizeAnswers(answers) var wg sync.WaitGroup row := 2 for session := range results { wg.Add(1) go func(session string, response []model.Answer, row int) { defer wg.Done() processSession(file, sheet, session, s3Prefix, response, results, questions, mapQueRes, headers, row) }(session, standart[session], row) row++ } wg.Wait() return file.Write(buffer) } func prepareHeaders(questions []model.Question) ([]string, map[uint64]string) { headers := []string{"Данные респондента"} mapQueRes := make(map[uint64]string) for _, q := range questions { if !q.Deleted { if q.Type == model.TypeResult { mapQueRes[q.Id] = q.Title + "\n" + q.Description } else { headers = append(headers, q.Title) } } } headers = append(headers, "Результат") return headers, mapQueRes } func categorizeAnswers(answers []model.Answer) (map[string][]model.Answer, map[string]model.Answer) { standart := make(map[string][]model.Answer) results := make(map[string]model.Answer) for _, answer := range answers { if answer.Result { results[answer.Session] = answer } else { standart[answer.Session] = append(standart[answer.Session], answer) } } return standart, results } func processSession(file *excelize.File, sheet, session, s3Prefix string, response []model.Answer, results map[string]model.Answer, questions []model.Question, mapQueRes map[uint64]string, headers []string, row int) { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].Content); err != nil { fmt.Println(err.Error()) } count := 2 for _, q := range questions { if !q.Deleted && q.Type != model.TypeResult { cell := ToAlphaString(count) + strconv.Itoa(row) index := binarySearch(response, q.Id) if index != -1 { handleAnswer(file, sheet, cell, s3Prefix, response[index], q, count, row) } else { if err := file.SetCellValue(sheet, cell, "-"); err != nil { fmt.Println(err.Error()) } } count++ } } cell := ToAlphaString(len(headers)) + strconv.Itoa(row) if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil { fmt.Println(err.Error()) } } func handleAnswer(file *excelize.File, sheet, cell, s3Prefix string, answer model.Answer, question model.Question, count, row int) { tipe := FileSearch(answer.Content) noAccept := make(map[string]struct{}) todoMap := make(map[string]string) if tipe != "Text" && (question.Type == model.TypeImages || question.Type == model.TypeVarImages) { handleImage(file, sheet, cell, answer.Content, count, row, noAccept, todoMap, question.Title) } else if question.Type == model.TypeFile { handleFile(file, sheet, cell, answer.Content, s3Prefix, noAccept) } else { todoMap[answer.Content] = cell } for cnt, cel := range todoMap { if _, ok := noAccept[cnt]; !ok { cntArr := strings.Split(cnt, "`,`") resultCnt := cnt if len(cntArr) > 1 { resultCnt = strings.Join(cntArr, "\n") } if len(resultCnt) > 1 && resultCnt[0] == '`' && resultCnt[len(resultCnt)-1] == '`' { resultCnt = resultCnt[1 : len(resultCnt)-1] } if len(resultCnt) > 1 && resultCnt[0] == '`' { resultCnt = resultCnt[1:] } if len(resultCnt) > 1 && resultCnt[len(resultCnt)-1] == '`' { resultCnt = resultCnt[:len(resultCnt)-1] } if err := file.SetCellValue(sheet, cel, resultCnt); err != nil { fmt.Println(err.Error()) } } } } func handleImage(file *excelize.File, sheet, cell, content string, count, row int, noAccept map[string]struct{}, todoMap map[string]string, questionTitle string) { multiImgArr := strings.Split(content, "`,`") if len(multiImgArr) > 1 { var descriptions []string mediaSheet := "Media" flag, err := file.GetSheetIndex(mediaSheet) if err != nil { fmt.Println(err.Error()) } if flag == -1 { _, _ = file.NewSheet(mediaSheet) err = file.SetCellValue(mediaSheet, "A1", "Вопрос") if err != nil { fmt.Println(err.Error()) } } mediaRow := row for i, imgContent := range multiImgArr { if i == 0 && len(imgContent) > 1 && imgContent[0] == '`' { imgContent = imgContent[1:] } if i == len(multiImgArr)-1 && len(imgContent) > 1 && imgContent[len(imgContent)-1] == '`' { imgContent = imgContent[:len(imgContent)-1] } var res model.ImageContent err := json.Unmarshal([]byte(imgContent), &res) if err != nil { res.Image = imgContent } // чек на пустой дескрипшен, есмли пустой то отмечаем как вариант ответа номер по i if res.Description != "" { descriptions = append(descriptions, res.Description) } else { descriptions = append(descriptions, fmt.Sprintf("Вариант ответа №%d", i+1)) } urle := ExtractImageURL(res.Image) urlData := strings.Split(urle, " ") if len(urlData) == 1 { u, err := url.Parse(urle) if err == nil && u.Scheme != "" && u.Host != "" { picture, err := downloadImage(urle) if err != nil { fmt.Println(err.Error()) continue } err = file.SetCellValue(mediaSheet, "A"+strconv.Itoa(mediaRow), questionTitle) if err != nil { fmt.Println(err.Error()) } col := ToAlphaString(i + 2) err = file.SetColWidth(mediaSheet, col, col, 50) if err != nil { fmt.Println(err.Error()) } err = file.SetRowHeight(mediaSheet, mediaRow, 150) if err != nil { fmt.Println(err.Error()) } if err := file.AddPictureFromBytes(mediaSheet, col+strconv.Itoa(mediaRow), picture); err != nil { fmt.Println(err.Error()) } noAccept[content] = struct{}{} } else { todoMap[content] = cell } } else { todoMap[imgContent] = cell } descriptionsStr := strings.Join(descriptions, "\n") linkText := fmt.Sprintf("%s\n Перейти в приложение", descriptionsStr) if err := file.SetCellValue(sheet, cell, linkText); err != nil { fmt.Println(err.Error()) } if err := file.SetCellHyperLink(sheet, cell, fmt.Sprintf("%s!A%d", mediaSheet, mediaRow), "Location", excelize.HyperlinkOpts{ Display: &linkText, }); err != nil { fmt.Println(err.Error()) } } } else { if len(content) > 1 && content[0] == '`' && content[len(content)-1] == '`' { content = content[1 : len(content)-1] } var res model.ImageContent err := json.Unmarshal([]byte(content), &res) if err != nil { res.Image = content } urle := ExtractImageURL(res.Image) urlData := strings.Split(urle, " ") if len(urlData) == 1 { u, err := url.Parse(urle) if err == nil && u.Scheme != "" && u.Host != "" { picture, err := downloadImage(urle) if err != nil { fmt.Println(err.Error()) } err = file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50) if err != nil { fmt.Println(err.Error()) } err = file.SetRowHeight(sheet, row, 150) if err != nil { fmt.Println(err.Error()) } if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil { fmt.Println(err.Error()) } noAccept[content] = struct{}{} } else { todoMap[content] = cell } } else { todoMap[content] = cell } } } func handleFile(file *excelize.File, sheet, cell, content, s3Prefix string, noAccept map[string]struct{}) { urle := content if urle != "" && !strings.HasPrefix(urle, "https") { urle = s3Prefix + urle } fmt.Println("ORRRRR", urle, s3Prefix) display, tooltip := urle, urle if err := file.SetCellValue(sheet, cell, urle); err != nil { fmt.Println(err.Error()) } if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{ Display: &display, Tooltip: &tooltip, }); err != nil { fmt.Println(err.Error()) } noAccept[content] = struct{}{} } func binarySearch(answers []model.Answer, questionID uint64) int { left := 0 right := len(answers) - 1 for left <= right { mid := left + (right-left)/2 if answers[mid].QuestionId == questionID { return mid } else if answers[mid].QuestionId < questionID { left = mid + 1 } else { right = mid - 1 } } return -1 } func FileSearch(content string) string { if strings.Contains(content, bucketImages) { return FileType(content) } else if strings.Contains(content, bucketFonts) { return FileType(content) } else if strings.Contains(content, bucketScripts) { return FileType(content) } else if strings.Contains(content, bucketStyle) { return FileType(content) } else if strings.Contains(content, bucketAnswers) { return FileType(content) } return "Text" } func FileType(filename string) string { parts := strings.Split(filename, ".") extension := parts[len(parts)-1] switch extension { case "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "tiff", "ico": return "Image" default: return "File" } } func downloadImage(url string) (*excelize.Picture, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() imgData, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } ext := filepath.Ext(url) if ext == "" { contentType := resp.Header.Get("Content-Type") switch { case strings.HasPrefix(contentType, "image/jpeg"): ext = ".jpg" case strings.HasPrefix(contentType, "image/png"): ext = ".png" default: ext = ".png" } } pic := &excelize.Picture{ Extension: ext, File: imgData, Format: &excelize.GraphicOptions{ AutoFit: true, Positioning: "oneCell", }, } return pic, nil } func ToAlphaString(col int) string { var result string for col > 0 { col-- result = string(rune('A'+col%26)) + result col /= 26 } return result } func ExtractImageURL(htmlContent string) string { re := regexp.MustCompile(`(?:]*src="([^"]+)"[^>]*>)|(?:]*>.*?]*src="([^"]+)"[^>]*>.*?)|(?:]*>.*?]*>.*?]*src="([^"]+)"[^>]*>.*?.*?)|(?:]*\s+download[^>]*>([^<]+)<\/a>)`) matches := re.FindAllStringSubmatch(htmlContent, -1) for _, match := range matches { for i := 1; i < len(match); i++ { if match[i] != "" { return strings.TrimSpace(match[i]) } } } return htmlContent }