misc.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package handler
  15. import (
  16. "bytes"
  17. "context"
  18. "encoding/csv"
  19. "fmt"
  20. "net/http"
  21. "strconv"
  22. "strings"
  23. "time"
  24. "github.com/360EntSecGroup-Skylar/excelize"
  25. "golang.org/x/sync/errgroup"
  26. "yunion.io/x/jsonutils"
  27. "yunion.io/x/log"
  28. "yunion.io/x/pkg/appctx"
  29. "yunion.io/x/pkg/util/httputils"
  30. "yunion.io/x/pkg/utils"
  31. "yunion.io/x/onecloud/pkg/apigateway/options"
  32. "yunion.io/x/onecloud/pkg/appsrv"
  33. "yunion.io/x/onecloud/pkg/httperrors"
  34. "yunion.io/x/onecloud/pkg/mcclient"
  35. "yunion.io/x/onecloud/pkg/mcclient/auth"
  36. "yunion.io/x/onecloud/pkg/mcclient/modulebase"
  37. "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
  38. "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
  39. )
  40. const contentTypeSpreadsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  41. const (
  42. HOST_MAC = "*MAC地址"
  43. HOST_NAME = "*名称"
  44. HOST_IPMI_ADDR = "*IPMI地址"
  45. HOST_IPMI_USERNAME = "*IPMI用户名"
  46. HOST_IPMI_PASSWORD = "*IPMI密码"
  47. HOST_MNG_IP_ADDR = "*管理口IP地址"
  48. HOST_IPMI_ADDR_OPTIONAL = "IPMI地址"
  49. HOST_IPMI_USERNAME_OPTIONAL = "IPMI用户名"
  50. HOST_IPMI_PASSWORD_OPTIONAL = "IPMI密码"
  51. HOST_MNG_IP_ADDR_OPTIONAL = "管理口IP地址"
  52. HOST_MNG_MAC_ADDR_OPTIONAL = "管理口MAC地址"
  53. )
  54. const (
  55. BATCH_USER_REGISTER_QUANTITY_LIMITATION = 1000
  56. BATCH_HOST_REGISTER_QUANTITY_LIMITATION = 1000
  57. )
  58. var (
  59. BatchHostRegisterTemplate = []string{HOST_MAC, HOST_NAME, HOST_IPMI_ADDR_OPTIONAL, HOST_IPMI_USERNAME_OPTIONAL, HOST_IPMI_PASSWORD_OPTIONAL}
  60. BatchHostISORegisterTemplate = []string{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_IP_ADDR}
  61. BatchHostPXERegisterTemplate = []string{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_MAC_ADDR_OPTIONAL, HOST_MNG_IP_ADDR_OPTIONAL}
  62. )
  63. func FetchSession(ctx context.Context, r *http.Request) *mcclient.ClientSession {
  64. token := AppContextToken(ctx)
  65. session := auth.GetSession(ctx, token, FetchRegion(r))
  66. return session
  67. }
  68. type MiscHandler struct {
  69. prefix string
  70. }
  71. func NewMiscHandler(prefix string) *MiscHandler {
  72. return &MiscHandler{prefix}
  73. }
  74. func (h *MiscHandler) GetPrefix() string {
  75. return h.prefix
  76. }
  77. func (h *MiscHandler) Bind(app *appsrv.Application) {
  78. prefix := h.prefix
  79. uploader := UploadHandlerInfo(POST, prefix+"uploads", FetchAuthToken(h.PostUploads))
  80. app.AddHandler3(uploader)
  81. app.AddHandler(GET, prefix+"downloads/<template_id>", FetchAuthToken(h.getDownloadsHandler))
  82. // download vm image by url (no auth token required). token 有效期24小时
  83. imageDownloadByUrl := uploadHandlerInfo(GET, prefix+"imageutils/image/<image_name>", imageDownloadByUrlHandler)
  84. app.AddHandler3(imageDownloadByUrl)
  85. // fetch vm image download url or download large file directly with query parameter <direct=true>
  86. imageDownloader := uploadHandlerInfo("GET", prefix+"imageutils/download/<image_id>", FetchAuthToken(imageDownloadHandler))
  87. app.AddHandler3(imageDownloader)
  88. imageUploader := uploadHandlerInfo("POST", prefix+"imageutils/upload", FetchAuthToken(imageUploadHandler))
  89. app.AddHandler3(imageUploader)
  90. s3upload := uploadHandlerInfo(POST, prefix+"s3uploads", FetchAuthToken(h.postS3UploadHandler))
  91. app.AddHandler3(s3upload)
  92. // mcp agent chat stream
  93. chatStream := chatHandlerInfo("POST", prefix+"mcp_agents/<id>/chat-stream", FetchAuthToken(mcpAgentChatStreamHandler))
  94. app.AddHandler3(chatStream)
  95. // mcp agent default chat stream (uses agent with default_agent=true)
  96. defaultChatStream := chatHandlerInfo("POST", prefix+"mcp_agents/default/chat-stream", FetchAuthToken(mcpAgentDefaultChatStreamHandler))
  97. app.AddHandler3(defaultChatStream)
  98. // syslog webservice handlers
  99. app.AddHandler(POST, prefix+"syslog/token", handleSyslogWebServiceToken)
  100. app.AddHandler(POST, prefix+"syslog/message", handleSyslogWebServiceMessage)
  101. // service settings
  102. app.AddHandler(GET, prefix+"service_settings", h.getServiceSettings)
  103. // mcp servers config
  104. app.AddHandler(GET, prefix+"mcp-servers-config", mcpServersConfigHandler)
  105. // mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry)
  106. app.AddHandler(GET, prefix+"default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler))
  107. }
  108. func UploadHandlerInfo(method, prefix string, handler func(context.Context, http.ResponseWriter, *http.Request)) *appsrv.SHandlerInfo {
  109. hi := appsrv.SHandlerInfo{}
  110. hi.SetMethod(method)
  111. hi.SetPath(prefix)
  112. hi.SetHandler(handler)
  113. hi.SetProcessTimeout(6 * time.Hour)
  114. hi.SetWorkerManager(GetUploaderWorker())
  115. return &hi
  116. }
  117. func (mh *MiscHandler) PostUploads(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  118. // 10 MB
  119. var maxMemory int64 = 10 << 20
  120. e := req.ParseMultipartForm(maxMemory)
  121. if e != nil {
  122. httperrors.InvalidInputError(ctx, w, "invalid form")
  123. return
  124. }
  125. params := req.MultipartForm.Value
  126. actions, ok := params["action"]
  127. if !ok || len(actions) == 0 || len(actions[0]) == 0 {
  128. err := httperrors.NewInputParameterError("Missing parameter %s", "action")
  129. httperrors.JsonClientError(ctx, w, err)
  130. return
  131. }
  132. switch actions[0] {
  133. // 主机批量注册
  134. case "BatchHostRegister":
  135. mh.DoBatchHostRegister(ctx, w, req)
  136. return
  137. // 用户批量注册
  138. case "BatchUserRegister":
  139. mh.DoBatchUserRegister(ctx, w, req)
  140. return
  141. default:
  142. err := httperrors.NewInputParameterError("Unsupported action %s", actions[0])
  143. httperrors.JsonClientError(ctx, w, err)
  144. return
  145. }
  146. }
  147. func (mh *MiscHandler) DoBatchHostRegister(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  148. files := req.MultipartForm.File
  149. hostfiles, ok := files["hosts"]
  150. if !ok || len(hostfiles) == 0 || hostfiles[0] == nil {
  151. e := httperrors.NewInputParameterError("Missing parameter %s", "hosts")
  152. httperrors.JsonClientError(ctx, w, e)
  153. return
  154. }
  155. fileHeader := hostfiles[0].Header
  156. contentType := fileHeader.Get("Content-Type")
  157. if contentType != contentTypeSpreadsheet {
  158. e := httperrors.NewInputParameterError("Wrong content type %s, want %s", contentType, contentTypeSpreadsheet)
  159. httperrors.JsonClientError(ctx, w, e)
  160. return
  161. }
  162. file, err := hostfiles[0].Open()
  163. defer file.Close()
  164. if err != nil {
  165. log.Errorf("%s", err.Error())
  166. e := httperrors.NewInternalServerError("can't open file")
  167. httperrors.JsonClientError(ctx, w, e)
  168. return
  169. }
  170. xlsx, err := excelize.OpenReader(file)
  171. if err != nil {
  172. log.Errorf("%s", err.Error())
  173. e := httperrors.NewInternalServerError("can't parse file")
  174. httperrors.JsonClientError(ctx, w, e)
  175. return
  176. }
  177. rows := xlsx.GetRows("hosts")
  178. if len(rows) == 0 {
  179. e := httperrors.NewGeneralError(fmt.Errorf("empty file content"))
  180. httperrors.JsonClientError(ctx, w, e)
  181. return
  182. }
  183. // check header line
  184. titlesOk := false
  185. for _, t := range [][]string{BatchHostRegisterTemplate, BatchHostISORegisterTemplate, BatchHostPXERegisterTemplate} {
  186. if len(t) == len(rows[0]) {
  187. for _, title := range rows[0] {
  188. if !utils.IsInStringArray(title, t) {
  189. break
  190. }
  191. }
  192. titlesOk = true
  193. }
  194. }
  195. if !titlesOk {
  196. httperrors.InputParameterError(ctx, w, "template file is invalid. please check.")
  197. return
  198. }
  199. paramKeys := []string{}
  200. i1 := -1
  201. i2 := -1
  202. for i, title := range rows[0] {
  203. switch title {
  204. case HOST_MAC, HOST_MNG_MAC_ADDR_OPTIONAL:
  205. paramKeys = append(paramKeys, "access_mac")
  206. case HOST_NAME:
  207. paramKeys = append(paramKeys, "name")
  208. case HOST_IPMI_ADDR, HOST_IPMI_ADDR_OPTIONAL:
  209. i1 = i
  210. paramKeys = append(paramKeys, "ipmi_ip_addr")
  211. case HOST_IPMI_USERNAME, HOST_IPMI_USERNAME_OPTIONAL:
  212. paramKeys = append(paramKeys, "ipmi_username")
  213. case HOST_IPMI_PASSWORD, HOST_IPMI_PASSWORD_OPTIONAL:
  214. paramKeys = append(paramKeys, "ipmi_password")
  215. case HOST_MNG_IP_ADDR, HOST_MNG_IP_ADDR_OPTIONAL:
  216. i2 = i
  217. paramKeys = append(paramKeys, "access_ip")
  218. default:
  219. e := httperrors.NewInternalServerError("empty file content")
  220. httperrors.JsonClientError(ctx, w, e)
  221. return
  222. }
  223. }
  224. // skipped header row
  225. if len(rows) > BATCH_HOST_REGISTER_QUANTITY_LIMITATION {
  226. e := httperrors.NewInputParameterError("beyond limitation. excel file rows must less than %d", BATCH_HOST_REGISTER_QUANTITY_LIMITATION)
  227. httperrors.JsonClientError(ctx, w, e)
  228. return
  229. }
  230. ips := []string{}
  231. hosts := bytes.Buffer{}
  232. for idx, row := range rows[1:] {
  233. rowStr := strings.Join(row, "")
  234. if len(rowStr) == 0 {
  235. log.Warningf("empty row: %d, skipping it", idx+1)
  236. continue
  237. }
  238. var e *httputils.JSONClientError
  239. if i1 >= 0 && len(row[i1]) > 0 {
  240. i1Ip := fmt.Sprintf("%d-%s", i1, row[i1])
  241. if utils.IsInStringArray(i1Ip, ips) {
  242. e = httperrors.NewDuplicateIdError("ip", row[i1])
  243. } else {
  244. ips = append(ips, i1Ip)
  245. }
  246. }
  247. if i2 >= 0 && len(row[i2]) > 0 {
  248. i2Ip := fmt.Sprintf("%d-%s", i2, row[i2])
  249. if utils.IsInStringArray(i2Ip, ips) {
  250. e = httperrors.NewDuplicateIdError("ip", row[i2])
  251. } else {
  252. ips = append(ips, i2Ip)
  253. }
  254. }
  255. if e != nil {
  256. httperrors.JsonClientError(ctx, w, e)
  257. return
  258. }
  259. hosts.WriteString(strings.Join(row, ",") + "\n")
  260. }
  261. params := jsonutils.NewDict()
  262. s := FetchSession(ctx, req)
  263. params.Set("hosts", jsonutils.NewString(hosts.String()))
  264. // extra params
  265. for k, values := range req.MultipartForm.Value {
  266. if len(values) > 0 && k != "action" {
  267. params.Set(k, jsonutils.NewString(values[0]))
  268. }
  269. }
  270. submitResult, err := compute.Hosts.BatchRegister(s, paramKeys, params)
  271. if err != nil {
  272. e := httperrors.NewGeneralError(err)
  273. httperrors.JsonClientError(ctx, w, e)
  274. return
  275. }
  276. w.WriteHeader(207)
  277. appsrv.SendJSON(w, modulebase.SubmitResults2JSON(submitResult))
  278. }
  279. func (mh *MiscHandler) DoBatchUserRegister(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  280. adminS := auth.GetAdminSession(ctx, FetchRegion(req))
  281. s := FetchSession(ctx, req)
  282. files := req.MultipartForm.File
  283. userfiles, ok := files["users"]
  284. if !ok || len(userfiles) == 0 || userfiles[0] == nil {
  285. e := httperrors.NewInputParameterError("Missing parameter %s", "users")
  286. httperrors.JsonClientError(ctx, w, e)
  287. return
  288. }
  289. fileHeader := userfiles[0].Header
  290. contentType := fileHeader.Get("Content-Type")
  291. if contentType != contentTypeSpreadsheet {
  292. e := httperrors.NewInputParameterError("Wrong content type %s, want %s", contentType, contentTypeSpreadsheet)
  293. httperrors.JsonClientError(ctx, w, e)
  294. return
  295. }
  296. file, err := userfiles[0].Open()
  297. defer file.Close()
  298. if err != nil {
  299. log.Errorf("%s", err.Error())
  300. e := httperrors.NewInternalServerError("can't open file")
  301. httperrors.JsonClientError(ctx, w, e)
  302. return
  303. }
  304. xlsx, err := excelize.OpenReader(file)
  305. if err != nil {
  306. log.Errorf("%s", err.Error())
  307. e := httperrors.NewInternalServerError("can't parse file")
  308. httperrors.JsonClientError(ctx, w, e)
  309. return
  310. }
  311. // skipped header row
  312. rows := xlsx.GetRows("users")
  313. if len(rows) <= 1 {
  314. e := httperrors.NewInputParameterError("empty file content")
  315. httperrors.JsonClientError(ctx, w, e)
  316. return
  317. } else if len(rows) > BATCH_USER_REGISTER_QUANTITY_LIMITATION {
  318. e := httperrors.NewInputParameterError("beyond limitation.excel file rows must less than %d", BATCH_USER_REGISTER_QUANTITY_LIMITATION)
  319. httperrors.JsonClientError(ctx, w, e)
  320. return
  321. }
  322. users := []jsonutils.JSONObject{}
  323. names := map[string]bool{}
  324. domains := map[string]string{}
  325. for i, row := range rows[1:] {
  326. rowIdx := i + 2
  327. name := row[0]
  328. password := row[1]
  329. displayname := row[2]
  330. domain := row[3]
  331. allowWebConsole := strings.ToLower(row[4])
  332. // 忽略空白行
  333. rowStr := strings.Join(row, "")
  334. if len(strings.TrimSpace(rowStr)) == 0 {
  335. continue
  336. }
  337. if len(name) == 0 {
  338. e := httperrors.NewClientError("row %d name is empty", rowIdx)
  339. httperrors.JsonClientError(ctx, w, e)
  340. return
  341. }
  342. if len(displayname) == 0 {
  343. displayname = name
  344. }
  345. if len(password) == 0 {
  346. e := httperrors.NewClientError("row %d password is empty", rowIdx)
  347. httperrors.JsonClientError(ctx, w, e)
  348. return
  349. }
  350. domainId, ok := domains[domain]
  351. if !ok {
  352. if len(domain) == 0 {
  353. e := httperrors.NewClientError("row %d domain is empty", rowIdx)
  354. httperrors.JsonClientError(ctx, w, e)
  355. return
  356. }
  357. id, err := identity.Domains.GetId(adminS, domain, nil)
  358. if err != nil {
  359. httperrors.JsonClientError(ctx, w, httperrors.NewGeneralError(err))
  360. return
  361. }
  362. domainId = id
  363. domains[domain] = id
  364. }
  365. if _, ok := names[name+"/"+domainId]; ok {
  366. e := httperrors.NewClientError("row %d duplicate name %s", rowIdx, name)
  367. httperrors.JsonClientError(ctx, w, e)
  368. return
  369. } else {
  370. names[name+"/"+domainId] = true
  371. params := jsonutils.NewDict()
  372. params.Set("domain_id", jsonutils.NewString(domainId))
  373. _, err := identity.UsersV3.Get(s, name, params)
  374. if err == nil {
  375. continue
  376. }
  377. }
  378. user := jsonutils.NewDict()
  379. user.Add(jsonutils.NewString(name), "name")
  380. user.Add(jsonutils.NewString(displayname), "displayname")
  381. user.Add(jsonutils.NewString(domainId), "domain_id")
  382. if len(password) > 0 {
  383. user.Add(jsonutils.NewString(password), "password")
  384. user.Add(jsonutils.JSONTrue, "skip_password_complexity_check")
  385. }
  386. if allowWebConsole == "true" || allowWebConsole == "1" {
  387. user.Add(jsonutils.JSONTrue, "allow_web_console")
  388. } else {
  389. user.Add(jsonutils.JSONFalse, "allow_web_console")
  390. }
  391. users = append(users, user)
  392. }
  393. // batch create
  394. var userG errgroup.Group
  395. for i := range users {
  396. user := users[i]
  397. userG.Go(func() error {
  398. _, err := identity.UsersV3.Create(s, user)
  399. return err
  400. })
  401. }
  402. if err := userG.Wait(); err != nil {
  403. e := httperrors.NewGeneralError(err)
  404. httperrors.GeneralServerError(ctx, w, e)
  405. return
  406. }
  407. appsrv.SendJSON(w, jsonutils.NewDict())
  408. }
  409. func (mh *MiscHandler) getDownloadsHandler(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  410. params := appctx.AppContextParams(ctx)
  411. template, ok := params["<template_id>"]
  412. if !ok || len(template) == 0 {
  413. httperrors.MissingParameterError(ctx, w, "template_id")
  414. return
  415. }
  416. var err error
  417. var content bytes.Buffer
  418. switch template {
  419. case "BatchHostRegister":
  420. records := [][]string{BatchHostRegisterTemplate}
  421. content, err = writeXlsx("hosts", records)
  422. case "BatchHostISORegister":
  423. records := [][]string{BatchHostISORegisterTemplate}
  424. content, err = writeXlsx("hosts", records)
  425. case "BatchHostPXERegister":
  426. records := [][]string{BatchHostPXERegisterTemplate}
  427. content, err = writeXlsx("hosts", records)
  428. case "BatchUserRegister":
  429. records := [][]string{{"*用户名(user)", "*用户密码(password)", "*显示名(displayname)", "*部门/域(domain)", "*是否登录控制台(allow_web_console:true、false)"}}
  430. content, err = writeXlsx("users", records)
  431. case "BatchProjectRegister":
  432. var titles []string
  433. if options.Options.NonDefaultDomainProjects {
  434. titles = []string{"项目名称", "域", "配额"}
  435. } else {
  436. titles = []string{"项目名称", "域"}
  437. }
  438. records := [][]string{titles}
  439. content, err = writeXlsx("projects", records)
  440. default:
  441. httperrors.InputParameterError(ctx, w, "template not found %s", template)
  442. return
  443. }
  444. if err != nil {
  445. httperrors.InternalServerError(ctx, w, "internal server error")
  446. return
  447. }
  448. w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
  449. w.Header().Set("Content-Disposition", "Attachment; filename=template.xlsx")
  450. w.Write(content.Bytes())
  451. return
  452. }
  453. func (mh *MiscHandler) postS3UploadHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  454. reader, e := r.MultipartReader()
  455. if e != nil {
  456. log.Debugf("postS3UploadHandler.MultipartReader %s", e)
  457. httperrors.InvalidInputError(ctx, w, "invalid form")
  458. return
  459. }
  460. p, f, e := readImageForm(reader)
  461. if e != nil {
  462. log.Debugf("postS3UploadHandler.readImageForm %s", e)
  463. httperrors.InvalidInputError(ctx, w, "invalid form")
  464. return
  465. }
  466. bucket_id, ok := p["bucket_id"]
  467. if !ok {
  468. httperrors.MissingParameterError(ctx, w, "bucket_id")
  469. return
  470. }
  471. key, ok := p["key"]
  472. if !ok {
  473. httperrors.MissingParameterError(ctx, w, "key")
  474. return
  475. }
  476. _content_length, ok := p["content_length"]
  477. if !ok {
  478. httperrors.MissingParameterError(ctx, w, "content_length")
  479. return
  480. }
  481. content_length, e := strconv.ParseInt(_content_length, 10, 64)
  482. if e != nil {
  483. httperrors.InvalidInputError(ctx, w, "invalid content_length %s", _content_length)
  484. return
  485. }
  486. storage_class, _ := p["storage_class"]
  487. acl, _ := p["acl"]
  488. token := AppContextToken(ctx)
  489. s := auth.GetSession(ctx, token, FetchRegion(r))
  490. meta := http.Header{}
  491. meta.Set("Content-Type", "application/octet-stream")
  492. e = compute.Buckets.Upload(s, bucket_id, key, f, content_length, storage_class, acl, meta)
  493. if e != nil {
  494. httperrors.GeneralServerError(ctx, w, e)
  495. return
  496. }
  497. appsrv.SendJSON(w, jsonutils.NewDict())
  498. }
  499. func writeCsv(records [][]string) (bytes.Buffer, error) {
  500. var content bytes.Buffer
  501. content.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM, 防止office打开后中文乱码
  502. writer := csv.NewWriter(&content)
  503. writer.WriteAll(records)
  504. if err := writer.Error(); err != nil {
  505. log.Errorf("error writing csv:%s", err.Error())
  506. return content, err
  507. }
  508. return content, nil
  509. }
  510. func writeXlsx(sheetName string, records [][]string) (bytes.Buffer, error) {
  511. var content bytes.Buffer
  512. xlsx := excelize.NewFile()
  513. xlsx.SetSheetName("Sheet1", sheetName)
  514. index := xlsx.GetSheetIndex(sheetName)
  515. for i, record := range records {
  516. xlsx.SetSheetRow(sheetName, fmt.Sprintf("A%d", i+1), &record)
  517. }
  518. xlsx.SetActiveSheet(index)
  519. if err := xlsx.Write(&content); err != nil {
  520. log.Errorf("error writing xlsx:%s", err.Error())
  521. return content, err
  522. }
  523. return content, nil
  524. }