package main import ( "archive/tar" "backend/md" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strconv" "strings" "time" ) type ArticleStatus = int const ( ArticleStatusDraft ArticleStatus = iota ArticleStatusPublished ArticleStatusOffline ) type ArticleProperties struct { Id int64 `json:"id"` Title string `json:"title"` Status ArticleStatus `json:"status"` Tags []string `json:"tags"` ReleaseDate time.Time `json:"date"` ModificationDate *time.Time `json:"mod-date"` } type Article struct { ArticleProperties Content string Files []ArticleFile } type ArticleFile struct { Id int Data []byte } func ParseArticle(reader io.Reader, filePrefix string) (*Article, error) { tarFiles := make(map[string][]byte) tarReader := tar.NewReader(reader) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return nil, err } content, err := io.ReadAll(tarReader) if err != nil { return nil, err } tarFiles[header.Name] = content } readmeBytes, found := tarFiles["README.md"] if !found { return nil, errors.New("README.md not found") } usedFiles := make(map[string]ArticleFile) nextFileId := 0 pc, html, err := md.Parse(func(path string) (string, error) { content, ok := tarFiles[strings.TrimPrefix(path, "./")] if !ok { return "", errors.New("file '" + path + "' not found") } usedFiles[path] = ArticleFile{nextFileId, content} nextFileId++ return filePrefix + strconv.Itoa(nextFileId), nil }, readmeBytes) if err != nil { return nil, err } title, err := md.GetProperty(pc, "title") if err != nil { if errors.Is(err, md.PropertyNotFoundError) { return nil, errors.New("title property not found") } return nil, err } tagsStr, err := md.GetProperty(pc, "tags") if err != nil && !errors.Is(err, md.PropertyNotFoundError) { return nil, err } tags := make([]string, 0) if strings.TrimSpace(tagsStr) != "" { for _, tagStr := range strings.Split(tagsStr, ",") { tags = append(tags, strings.TrimSpace(tagStr)) } } dateStr, err := md.GetProperty(pc, "date") if err != nil { if errors.Is(err, md.PropertyNotFoundError) { return nil, errors.New("date property not found") } return nil, err } releaseDate, err := time.Parse(time.DateOnly, dateStr) if err != nil { return nil, fmt.Errorf("invalid date property '%s': %v", dateStr, err) } var modificationDate *time.Time dateStr, err = md.GetProperty(pc, "mod-date") if err == nil { tmp, err := time.Parse(time.DateOnly, dateStr) if err != nil { return nil, fmt.Errorf("invalid mod-date property '%s': %v", dateStr, err) } modificationDate = &tmp } else if !errors.Is(err, md.PropertyNotFoundError) { return nil, err } files := make([]ArticleFile, 0, len(usedFiles)) for _, file := range usedFiles { files = append(files, file) } return &Article{ ArticleProperties{ -1, title, ArticleStatusDraft, tags, releaseDate, modificationDate, }, string(html), files, }, nil }