From 78624641e435798b14cefc2f07d2928f87747391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=91=D0=B0?= =?UTF-8?q?=D0=B4=D1=8F=D0=B5=D0=B2?= Date: Sun, 9 Apr 2023 21:08:35 +0700 Subject: [PATCH 1/2] =?UTF-8?q?=D0=92=D1=81=D0=B5=20=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=B1=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D1=8F=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=B8.=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR closes #1 Reviewed-on: https://git.mousesoft.ru/ms/drawio-export/pulls/3 --- cmd/drawio-export/main.go | 28 ++- cmd/drawio-export/options.go | 75 ++++++++ pkg/drawio/export.go | 125 ++++++++++++- pkg/drawio/options.go | 350 +++++++++++++++++++++++++++++++++++ 4 files changed, 575 insertions(+), 3 deletions(-) create mode 100644 cmd/drawio-export/options.go create mode 100644 pkg/drawio/options.go diff --git a/cmd/drawio-export/main.go b/cmd/drawio-export/main.go index 99fc8a2..a129101 100644 --- a/cmd/drawio-export/main.go +++ b/cmd/drawio-export/main.go @@ -1,7 +1,31 @@ package main -import "fmt" +import ( + "flag" + "fmt" + "os" + + "git.mousesoft.ru/ms/drawio-exporter/pkg/drawio" +) + +var version string // Версия приложения func main() { - fmt.Println("Draw.io Exporter") + flag.Parse() + if flagHelp { + flag.Usage() + os.Exit(0) + } + if flagVersion { + fmt.Println("Draw.io Export cli util", version) + os.Exit(0) + } + if flag.NArg() < 1 { + flag.Usage() + os.Exit(1) + } + exporter := drawio.NewWithOptions(&opts) + if err := exporter.Export(flag.Args()...); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } } diff --git a/cmd/drawio-export/options.go b/cmd/drawio-export/options.go new file mode 100644 index 0000000..0d34320 --- /dev/null +++ b/cmd/drawio-export/options.go @@ -0,0 +1,75 @@ +package main + +import ( + "flag" + + "git.mousesoft.ru/ms/drawio-exporter/pkg/drawio" +) + +var ( + flagHelp bool // Вывести справку о приложении и выйти + flagVersion bool // Вывести информацию о версии приложения и выйти + opts = drawio.Options{} // Аргументы командной строки приложения +) + +func init() { + // version + flag.BoolVar(&flagVersion, "V", false, "Prints version information") + flag.BoolVar(&flagVersion, "version", false, "Prints version information") + // Application + flag.StringVar(&opts.Application, "A", "", "Draw.io Desktop Application") + flag.StringVar(&opts.Application, "application", "", "Draw.io Desktop Application") + // Output + flag.StringVar(&opts.Output, "o", "", "Exported folder name [default: export]") + flag.StringVar(&opts.Output, "output", "", "Exported folder name [default: export]") + // Format + flag.Var(&opts.Format, "f", + "Exported format [default: pdf] [possible values: pdf, png, jpg, svg, vsdx, xml]") + flag.Var(&opts.Format, "format", + "Exported format [default: pdf] [possible values: pdf, png, jpg, svg, vsdx, xml]") + // Recursive + flag.BoolVar(&opts.Recursive, "r", false, + "For a folder input, recursively convert all files in sub-folders also") + flag.BoolVar(&opts.Recursive, "recursive", false, + "For a folder input, recursively convert all files in sub-folders also") + // RemovePageSuffix + flag.BoolVar(&opts.RemovePageSuffix, "remove-page-suffix", false, + "Remove page suffix when possible (in case of single page file") + // Quality + flag.UintVar(&opts.Quality, "q", 0, "Output image quality for JPEG [default: 90]") + flag.UintVar(&opts.Quality, "quality", 0, "Output image quality for JPEG [default: 90]") + // Transparent + flag.BoolVar(&opts.Transparent, "t", false, "Set transparent background for PNG") + flag.BoolVar(&opts.Transparent, "transparent", false, "Set transparent background for PNG") + // EmbedDiagram + flag.BoolVar(&opts.EmbedDiagram, "e", false, + "Includes a copy of the diagram for PDF, PNG, or SVG") + flag.BoolVar(&opts.EmbedDiagram, "embed-diagram", false, + "Includes a copy of the diagram for PDF, PNG, or SVG") + // EmbedSvgImages + flag.BoolVar(&opts.EmbedSvgImages, "embed-svg-images", false, "Embed Images in SVG file") + // Border + flag.UintVar(&opts.Border, "b", 0, + "Sets the border width around the diagram [default: 0]") + flag.UintVar(&opts.Border, "border", 0, + "Sets the border width around the diagram [default: 0]") + // Scale + flag.UintVar(&opts.Scale, "s", 0, "Scales the diagram size") + flag.UintVar(&opts.Scale, "scale", 0, "Scales the diagram size") + // Width + flag.UintVar(&opts.Width, "width", 0, + "Fits the generated image/pdf into the specified width, preserves aspect ratio") + // Height + flag.UintVar(&opts.Height, "height", 0, + "Fits the generated image/pdf into the specified height, preserves aspect ratio") + // Crop + flag.BoolVar(&opts.Crop, "crop", false, "Crops PDF to diagram size") + // Uncompressed + flag.BoolVar(&opts.Uncompressed, "u", false, "Uncompressed XML output") + flag.BoolVar(&opts.Uncompressed, "uncompressed", false, "Uncompressed XML output") + // EnablePlugins + flag.BoolVar(&opts.EnablePlugins, "enable-plugins", false, "Enable Plugins") + // help + flag.BoolVar(&flagHelp, "h", false, "Prints help information") + flag.BoolVar(&flagHelp, "help", false, "Prints help information") +} diff --git a/pkg/drawio/export.go b/pkg/drawio/export.go index 1e48b04..bf3e699 100644 --- a/pkg/drawio/export.go +++ b/pkg/drawio/export.go @@ -2,12 +2,67 @@ package drawio import ( "bufio" + "errors" "fmt" "io" + "io/fs" + "os" + "os/exec" + "path" + "strconv" + "strings" xmlparser "github.com/tamerh/xml-stream-parser" ) +// Создать нового экспортёра диаграмм +func New(opt ...Option) Exporter { + options := &Options{ + Application: "drawio", + Output: "export", + Format: Format("pdf"), + } + for _, opt := range opt { + opt.apply(options) + } + return NewWithOptions(options) +} + +// Создать нового экспортёра с параметрами +func NewWithOptions(opts *Options) Exporter { + return Exporter{opts: opts} +} + +// Экспортёр диаграмм +type Exporter struct { + opts *Options +} + +// Экспорт диаграмм из указанный файлов или папок +func (exp Exporter) Export(fileOrDir ...string) error { + var ( + err error + errs = []error{} + args = exp.opts.Args() + info fs.FileInfo + ) + for _, item := range fileOrDir { + if info, err = os.Stat(item); err != nil { + errs = append(errs, err) + continue + } + if info.IsDir() { + err = exp.exportDir(item, args) + } else { + err = exp.exportFile(item, args) + } + if err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + // Разбирает данные и возвращает срез имён диаграмм в них func Diagrams(reader io.Reader) ([]string, error) { var ( @@ -21,7 +76,6 @@ func Diagrams(reader io.Reader) ([]string, error) { if xml.Err != nil { return result, xml.Err } - fmt.Println(xml.Name) if items, ok := xml.Childs["diagram"]; ok { for _, item := range items { result = append(result, item.Attrs["name"]) @@ -30,3 +84,72 @@ func Diagrams(reader io.Reader) ([]string, error) { } return result, nil } + +// Экспорт диаграмм из всех файлов в папке +func (exp Exporter) exportDir(dirPath string, args []string, subDir ...string) error { + var ( + entries []fs.DirEntry + err error + errs []error + ) + if entries, err = os.ReadDir(dirPath); err != nil { + return err + } + for _, entry := range entries { + name := entry.Name() + outDir := make([]string, len(subDir), len(subDir)+1) + outDir = append(outDir, name) + if entry.IsDir() && exp.opts.Recursive { + err = exp.exportDir(path.Join(dirPath, name), args, outDir...) + } else { + err = exp.exportFile(path.Join(dirPath, name), args, outDir...) + } + if err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +// Экспорт файла с диаграммами +func (exp Exporter) exportFile(filePath string, args []string, subDir ...string) error { + var ( + file *os.File + diagrams []string + err error + ) + if file, err = os.Open(filePath); err != nil { + return err + } + defer file.Close() + if diagrams, err = Diagrams(file); err != nil { + return err + } + outDirs := make([]string, 1, len(subDir)+1) + outDirs[0] = exp.opts.Output + outDirs = append(outDirs, subDir...) + output := path.Join(outDirs...) + errs := []error{} + for i, name := range diagrams { + outName := strings.TrimSuffix(path.Base(filePath), path.Ext(filePath)) + if !exp.opts.RemovePageSuffix || len(diagrams) > 1 { + outName += "-" + name + } + outName += exp.opts.OutExt() + drawioArgs := make([]string, len(args), len(args)+4) + copy(drawioArgs, args) + drawioArgs = append(drawioArgs, + "--page-index", strconv.Itoa(i), + "--output", path.Join(output, outName), + "--export", filePath, + ) + cmd := exec.Command(exp.opts.App(), drawioArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fmt.Println("Run command:", cmd.Path, cmd.Args) + if err = cmd.Run(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} diff --git a/pkg/drawio/options.go b/pkg/drawio/options.go new file mode 100644 index 0000000..2249cf0 --- /dev/null +++ b/pkg/drawio/options.go @@ -0,0 +1,350 @@ +package drawio + +import ( + "flag" + "fmt" + "strconv" +) + +// Интерфейс параметра экспортёра диаграмм +type Option interface { + apply(opts *Options) // Применить параметр к хранилищу параметров +} + +// Ошибка "неподдерживаемый формат" +type ErrUnsupportedFormat struct { + Format string +} + +// Описание ошибки +func (e ErrUnsupportedFormat) Error() string { + return fmt.Sprintf("unsupported format: '%s'", e.Format) +} + +var _ error = (*ErrUnsupportedFormat)(nil) // Проверка реализации интерфейса + +type Format string // Формат экспортированного файла + +// Строковое представление формата +func (f Format) String() string { + return string(f) +} + +// Загрузка формата из строки +func (f *Format) Set(str string) error { + for _, fmt := range SupportedFormats() { + if str == string(fmt) { + *f = Format(str) + return nil + } + } + return &ErrUnsupportedFormat{str} +} + +var _ flag.Value = (*Format)(nil) // Поверка реализации интерфейса + +// Расширение файла заданного формата +func (f Format) ext() string { + return "." + string(f) +} + +// Параметры экспортёра диаграмм +type Options struct { + Application string // Путь к приложению drawio + Output string // Путь к папке с экспортированными файлами + Format Format // Формат экспортированных файлов + Recursive bool // Рекурсивно сканировать вложенные папки с файлами + RemovePageSuffix bool // Удалять суффикс страницы, если это возможно + Quality uint // Качество экспортированного изображения (только для JPEG) + Transparent bool // Прозрачный фона для PNG + // Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG + EmbedDiagram bool + EmbedSvgImages bool // Встраивать изображения в файл формата SVG + Border uint // Ширина рамки вокруг диаграмм + Scale uint // Масштаб в процентах размера экспортированных диаграмм + Width uint // Ширина экспортированной диаграммы с сохранением масштаба + Height uint // Высота экспортированной диаграммы с сохранением масштаба + Crop bool // Обрезать результирующий PDF до размера диаграммы + Uncompressed bool // Выводить несжатый XML + EnablePlugins bool // Включить подключаемые модули +} + +// Путь к приложению drawio +func (opts Options) App() string { + app := opts.Application + if len(app) == 0 { + app = "drawio" + } + return app +} + +// Путь к папке с экспортированными файлами +func (opts Options) OutDir() string { + out := opts.Output + if len(out) == 0 { + out = "export" + } + return out +} + +// Расширение экспортированных файлов +func (opts Options) OutExt() string { + fmt := opts.Format + if len(fmt) == 0 { + fmt = Format("pdf") + } + return fmt.ext() +} + +// Формирование аргументов командной строки +func (opts Options) Args() []string { + args := []string{} + if len(opts.Format) > 0 { + args = append(args, "--format", string(opts.Format)) + } + if opts.Quality != 0 { + args = append(args, "--quality", strconv.Itoa(int(opts.Quality))) + } + if opts.Transparent { + args = append(args, "--transparent") + } + if opts.EmbedDiagram { + args = append(args, "--embed-diagram") + } + if opts.EmbedSvgImages { + args = append(args, "--embed-svg-images") + } + if opts.Border != 0 { + args = append(args, "--border", strconv.Itoa(int(opts.Border))) + } + if opts.Scale != 0 { + args = append(args, "--scale", strconv.Itoa(int(opts.Scale))) + } + if opts.Width != 0 { + args = append(args, "--width", strconv.Itoa(int(opts.Width))) + } + if opts.Height != 0 { + args = append(args, "--height", strconv.Itoa(int(opts.Height))) + } + if opts.Crop { + args = append(args, "--crop") + } + if opts.Uncompressed { + args = append(args, "--uncompressed") + } + if opts.EnablePlugins { + args = append(args, "--enable-plugins") + } + return args +} + +// Путь к приложению drawio +func WithAppPath(path string) Option { + return optionApplication(path) +} + +type optionApplication string + +var _ Option = (*optionApplication)(nil) // Проверка реализации интерфейса + +func (opt optionApplication) apply(opts *Options) { + opts.Application = string(opt) +} + +// Путь к папке с экспортированными файлами +func WithOutput(path string) Option { + return optionOutput(path) +} + +type optionOutput string + +var _ Option = (*optionOutput)(nil) // Проверка реализации интерфейса + +func (opt optionOutput) apply(opts *Options) { + opts.Output = string(opt) +} + +// Формат экспортированных файлов +func WithFormat(format Format) Option { + return format +} + +var _ Option = (*Format)(nil) // Проверка реализации интерфейса + +func (opt Format) apply(opts *Options) { + opts.Format = opt +} + +// Рекурсивно сканировать вложенные папки с файлами +func WithRecursive() Option { + return optionRecursive{} +} + +type optionRecursive struct{} + +var _ Option = optionRecursive{} // Проверка реализации интерфейса + +func (opt optionRecursive) apply(opts *Options) { + opts.Recursive = true +} + +// Удалять суффикс страницы, если это возможно +func WithRemovePageSuffix() Option { + return optionRemovePageSuffix{} +} + +type optionRemovePageSuffix struct{} + +var _ Option = optionRemovePageSuffix{} // Проверка реализации интерфейса + +func (opt optionRemovePageSuffix) apply(opts *Options) { + opts.RemovePageSuffix = true +} + +// Качество экспортированного изображения (только для JPEG) +func WithQuality(q int) Option { + return optionQuality(q) +} + +type optionQuality uint + +var _ Option = (*optionQuality)(nil) // Проверка реализации интерфейса + +func (opt optionQuality) apply(opts *Options) { + opts.Quality = uint(opt) +} + +// Прозрачный фона для PNG +func WithTransparent() Option { + return optionTransparent{} +} + +type optionTransparent struct{} + +var _ Option = optionTransparent{} // Проверка реализации интерфейса + +func (opt optionTransparent) apply(opts *Options) { + opts.Transparent = true +} + +// Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG +func WithEmbedDiagram() Option { + return optionEmbedDiagram{} +} + +type optionEmbedDiagram struct{} + +var _ Option = optionEmbedDiagram{} // Проверка реализации интерфейса + +func (opt optionEmbedDiagram) apply(opts *Options) { + opts.EmbedDiagram = true +} + +// Встраивать изображения в файл формата SVG +func WithEmbedSvgImages() Option { + return optionEmbedSvgImages{} +} + +type optionEmbedSvgImages struct{} + +var _ Option = optionEmbedSvgImages{} // Проверка реализации интерфейса + +func (opt optionEmbedSvgImages) apply(opts *Options) { + opts.EmbedSvgImages = true +} + +// Ширина рамки вокруг диаграмм +func WithBorder(border int) Option { + return optionBorder(border) +} + +type optionBorder uint + +var _ Option = (*optionBorder)(nil) // Проверка реализации интерфейса + +func (opt optionBorder) apply(opts *Options) { + opts.Border = uint(opt) +} + +// Масштаб в процентах размера экспортированных диаграмм +func WithScale(scale int) Option { + return optionScale(scale) +} + +type optionScale uint + +var _ Option = (*optionScale)(nil) // Проверка реализации интерфейса + +func (opt optionScale) apply(opts *Options) { + opts.Scale = uint(opt) +} + +// Ширина экспортированной диаграммы с сохранением масштаба +func WithWidth(width int) Option { + return optionWidth(width) +} + +type optionWidth uint + +var _ Option = (*optionWidth)(nil) // Проверка реализации интерфейса + +func (opt optionWidth) apply(opts *Options) { + opts.Width = uint(opt) +} + +// Высота экспортированной диаграммы с сохранением масштаба +func WithHeight(height int) Option { + return optionHeight(height) +} + +type optionHeight uint + +var _ Option = (*optionHeight)(nil) // Проверка реализации интерфейса + +func (opt optionHeight) apply(opts *Options) { + opts.Height = uint(opt) +} + +// Обрезать результирующий PDF до размера диаграммы +func WithCrop() Option { + return optionCrop{} +} + +type optionCrop struct{} + +var _ Option = optionCrop{} // Проверка реализации интерфейса + +func (opt optionCrop) apply(opts *Options) { + opts.Crop = true +} + +// Выводить несжатый XML +func WithUncompressed() Option { + return optionUncompressed{} +} + +type optionUncompressed struct{} + +var _ Option = optionUncompressed{} // Проверка реализации интерфейса + +func (opt optionUncompressed) apply(opts *Options) { + opts.Uncompressed = true +} + +// Включить подключаемые модули +func WithEnablePlugins() Option { + return optionEnablePlugins{} +} + +type optionEnablePlugins struct{} + +var _ Option = optionEnablePlugins{} // Проверка реализации интерфейса + +func (opt optionEnablePlugins) apply(opts *Options) { + opts.EnablePlugins = true +} + +// Список всех поддерживаемых форматов экспорта +func SupportedFormats() []Format { + return []Format{"pdf", "png", "jpg", "svg", "vsdx", "xml"} +} From 8d18d7291e2e7fcd9c4f4381ba99d66d47ef4ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=91=D0=B0?= =?UTF-8?q?=D0=B4=D1=8F=D0=B5=D0=B2?= Date: Wed, 12 Apr 2023 22:37:54 +0700 Subject: [PATCH 2/2] =?UTF-8?q?=D0=90=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B0=D1=8F=20=D1=81=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=BA=D0=B0=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20Debian=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR closes #2 Reviewed-on: https://git.mousesoft.ru/ms/drawio-export/pulls/4 --- .vscode/settings.json | 11 + CHANGELOG.md | 2 +- build/ci/Jenkinsfile | 75 ++-- build/package/debian/changelog | 5 + build/package/debian/control | 8 + .../msi/templates/LicenseAgreementDlg_HK.wxs | 36 ++ build/package/msi/templates/WixUI_HK.wxs | 60 +++ build/package/msi/templates/choco/LICENSE.txt | 9 + .../msi/templates/choco/VERIFICATION.txt | 10 + .../msi/templates/choco/chocolateyInstall.ps1 | 7 + .../templates/choco/chocolateyUninstall.ps1 | 6 + build/package/msi/templates/choco/pkg.nuspec | 38 ++ build/package/msi/templates/product.wxs | 212 ++++++++++ build/package/msi/wix.json | 37 ++ makefile | 58 ++- pkg/drawio/execution.go | 20 + pkg/drawio/export.go | 188 +++++---- pkg/drawio/export_test.go | 373 +++++++++++++++++- pkg/drawio/options.go | 105 ++++- pkg/drawio/options_internal_test.go | 105 +++++ pkg/drawio/options_test.go | 76 ++++ 21 files changed, 1292 insertions(+), 149 deletions(-) create mode 100644 build/package/debian/changelog create mode 100644 build/package/debian/control create mode 100644 build/package/msi/templates/LicenseAgreementDlg_HK.wxs create mode 100644 build/package/msi/templates/WixUI_HK.wxs create mode 100644 build/package/msi/templates/choco/LICENSE.txt create mode 100644 build/package/msi/templates/choco/VERIFICATION.txt create mode 100644 build/package/msi/templates/choco/chocolateyInstall.ps1 create mode 100644 build/package/msi/templates/choco/chocolateyUninstall.ps1 create mode 100644 build/package/msi/templates/choco/pkg.nuspec create mode 100644 build/package/msi/templates/product.wxs create mode 100644 build/package/msi/wix.json create mode 100644 pkg/drawio/execution.go create mode 100644 pkg/drawio/options_internal_test.go create mode 100644 pkg/drawio/options_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c0048e..715eb0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,23 +4,34 @@ "Версионирование", "версионируется", "Alek", + "Aleksei", + "Badiaev", "Childs", + "choco", "drawio", "errcheck", "gocov", "GOPATH", "honnef", + "INSTALLDIR", "jstemmer", "kisielk", "matm", "mousesoft", "mxfile", "nxfile", + "outdir", + "outext", "sashamelentyev", + "shlibs", + "STARTMENUSHORTCUT", "staticcheck", "stretchr", + "subdir", + "svvg", "tamerh", "usestdlibvars", + "wdir", "xmlparser" ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd562a..04852c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/). -## [0.1] - Не опубликовано +## [1.0] - Не опубликовано - **Добавлено** - Все параметры передаются программе `drawio` из пакета `drawio-desktop` без diff --git a/build/ci/Jenkinsfile b/build/ci/Jenkinsfile index ed62133..c4dcbaf 100644 --- a/build/ci/Jenkinsfile +++ b/build/ci/Jenkinsfile @@ -30,35 +30,35 @@ pipeline { ''' } } - // stage('Build Windows') { - // agent{ label 'windows' } - // environment { - // GO_OPT = ' ' - // CGO_ENABLED = 0 - // } - // steps { - // echo "***** BUILD ${PROJECT_NAME} on Windows *****" - // cleanWs(disableDeferredWipeout: true, deleteDirs: true) - // catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') { - // checkout scm - // bat 'make clean vendor build dist pkg-msi' - // } - // } - // post { - // always { - // script { - // if (getContext(hudson.FilePath)) { - // archiveArtifacts ( - // artifacts: 'out/*.zip,out/*.msi', - // allowEmptyArchive: true, - // fingerprint: true, - // onlyIfSuccessful: true, - // ) - // } - // } - // } - // } - // } + stage('Build Windows') { + agent{ label 'windows' } + environment { + GO_OPT = ' ' + CGO_ENABLED = 0 + } + steps { + echo "***** BUILD ${PROJECT_NAME} on Windows *****" + cleanWs(disableDeferredWipeout: true, deleteDirs: true) + catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') { + checkout scm + bat 'make clean vendor build dist' + } + } + post { + always { + script { + if (getContext(hudson.FilePath)) { + archiveArtifacts ( + artifacts: 'out/*.zip,out/*.msi', + allowEmptyArchive: true, + fingerprint: true, + onlyIfSuccessful: true, + ) + } + } + } + } + } } } stage('Test') { @@ -117,15 +117,16 @@ pipeline { steps { echo "***** RELEASE ${PROJECT_NAME} for Linux *****" sh '''#!/bin/bash - make build dist + make build package + find -O1 dist/ -name '*.changes' -exec dput mousesoft \\{\\} \\; ''' script { - msUploadFilesToRelease( - "${PROJECT_OWNER}", - "${PROJECT_ID}", - RELEASE_ID, - ['dist/*.tar.gz', 'out/doc/*.pdf'], - ) + // msUploadFilesToRelease( + // "${PROJECT_OWNER}", + // "${PROJECT_ID}", + // RELEASE_ID, + // ['dist/*.deb'], + // ) if( currentBuild.currentResult == 'SUCCESS' ) { currentBuild.keepLog = true } @@ -143,7 +144,7 @@ pipeline { } steps { echo "***** RELEASE ${PROJECT_NAME} for Windows *****" - bat 'make build dist pkg-msi' + bat 'make build package' script { msUploadFilesToRelease( "${PROJECT_OWNER}", diff --git a/build/package/debian/changelog b/build/package/debian/changelog new file mode 100644 index 0000000..6c4dbfb --- /dev/null +++ b/build/package/debian/changelog @@ -0,0 +1,5 @@ +drawio-export (0.1) UNRELEASED; urgency=low + + * Initial Release. + + -- Aleksei Badiaev Sun, 9 Apr 2023 21:45:22 +0700 diff --git a/build/package/debian/control b/build/package/debian/control new file mode 100644 index 0000000..2d1b935 --- /dev/null +++ b/build/package/debian/control @@ -0,0 +1,8 @@ +Source: drawio-export +Maintainer: Aleksei Badiaev + +Package: drawio-export +Section: graphics +Priority: optional +Architecture: amd64 +Description: Export Draw.io diagrams using drawio-desktop. diff --git a/build/package/msi/templates/LicenseAgreementDlg_HK.wxs b/build/package/msi/templates/LicenseAgreementDlg_HK.wxs new file mode 100644 index 0000000..7b858d1 --- /dev/null +++ b/build/package/msi/templates/LicenseAgreementDlg_HK.wxs @@ -0,0 +1,36 @@ + + + + + + + + + + CostingComplete = 1 + "1"]]> + LicenseAccepted = "1" + + + 1 + + + + + {{if gt (.License | len) 0}} + + {{end}} + + + + 1 + + + + + + + + + diff --git a/build/package/msi/templates/WixUI_HK.wxs b/build/package/msi/templates/WixUI_HK.wxs new file mode 100644 index 0000000..1d7121f --- /dev/null +++ b/build/package/msi/templates/WixUI_HK.wxs @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + "1"]]> + + 1 + + NOT Installed + Installed AND PATCH + + 1 + LicenseAccepted = "1" + + 1 + 1 + NOT WIXUI_DONTVALIDATEPATH + "1"]]> + WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" + + 1 + 1 + + NOT Installed + Installed + + 1 + + 1 + 1 + 1 + + + + + diff --git a/build/package/msi/templates/choco/LICENSE.txt b/build/package/msi/templates/choco/LICENSE.txt new file mode 100644 index 0000000..c10af6d --- /dev/null +++ b/build/package/msi/templates/choco/LICENSE.txt @@ -0,0 +1,9 @@ +From: {{.Choco.LicenseURL}} + +LICENSE + +{{if gt (.License | len) 0}} +{{.License | cat}} +{{else if gt (.Choco.LicenseURL | len) 0}} +{{.Choco.LicenseURL | download}} +{{end}} diff --git a/build/package/msi/templates/choco/VERIFICATION.txt b/build/package/msi/templates/choco/VERIFICATION.txt new file mode 100644 index 0000000..7c399c9 --- /dev/null +++ b/build/package/msi/templates/choco/VERIFICATION.txt @@ -0,0 +1,10 @@ +VERIFICATION + +To check the checksum of this package, extract the msi file contained into it, +then run + + checksum.exe {{.Choco.MsiFile}} -t=sha256 + +The result must match + + {{.Choco.MsiSum | upper}} diff --git a/build/package/msi/templates/choco/chocolateyInstall.ps1 b/build/package/msi/templates/choco/chocolateyInstall.ps1 new file mode 100644 index 0000000..de61669 --- /dev/null +++ b/build/package/msi/templates/choco/chocolateyInstall.ps1 @@ -0,0 +1,7 @@ +$packageName = '{{.Choco.ID}}' +$fileType = 'msi' +$silentArgs = '/quiet'; +$scriptPath = $(Split-Path $MyInvocation.MyCommand.Path); +$fileFullPath = Join-Path $scriptPath '{{.Choco.MsiFile}}'; + +Install-ChocolateyInstallPackage $packageName $fileType $silentArgs $fileFullPath -checksum '{{.Choco.MsiSum}}' -checksumType = 'sha256' diff --git a/build/package/msi/templates/choco/chocolateyUninstall.ps1 b/build/package/msi/templates/choco/chocolateyUninstall.ps1 new file mode 100644 index 0000000..d4b26d0 --- /dev/null +++ b/build/package/msi/templates/choco/chocolateyUninstall.ps1 @@ -0,0 +1,6 @@ +$packageName = "{{.Choco.ID}}"; +$fileType = 'msi'; +$scriptPath = $(Split-Path $MyInvocation.MyCommand.Path); +$fileFullPath = Join-Path $scriptPath '{{.Choco.MsiFile}}'; + +Uninstall-ChocolateyPackage $packageName $fileType "$fileFullPath /q" diff --git a/build/package/msi/templates/choco/pkg.nuspec b/build/package/msi/templates/choco/pkg.nuspec new file mode 100644 index 0000000..146c13b --- /dev/null +++ b/build/package/msi/templates/choco/pkg.nuspec @@ -0,0 +1,38 @@ + + + + {{.Choco.ID}} + {{.Choco.Title}} + {{.Version.MSI}} + {{.Choco.Authors}} + {{.Choco.Owners}} + {{.Choco.Description}} + {{if gt (.Choco.ProjectURL | len) 0}} + {{.Choco.ProjectURL}} + {{end}} + {{if gt (.Choco.Tags | len) 0}} + {{.Choco.Tags}} + {{end}} + {{if gt (.Choco.LicenseURL | len) 0}} + {{.Choco.LicenseURL}} + {{end}} + {{if gt (.Choco.IconURL | len) 0}} + {{.Choco.IconURL}} + {{end}} + {{if gt (.Choco.ChangeLog | len) 0}} + {{.Choco.ChangeLog}} + {{end}} + {{if .Choco.RequireLicense}} + true + {{else}} + false + {{end}} + + + + + + + + + diff --git a/build/package/msi/templates/product.wxs b/build/package/msi/templates/product.wxs new file mode 100644 index 0000000..b4b41ed --- /dev/null +++ b/build/package/msi/templates/product.wxs @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + {{if gt (.Banner | len) 0 }} {{end}} + {{if gt (.Dialog | len) 0 }} {{end}} + + {{if gt (.Icon | len) 0 }} + + + {{end}} + + + + {{range $i, $p := .Properties}} + + {{if $p.Registry}} + + {{end}} + + {{end}} + {{range $i, $c := .Conditions}} + + {{end}} + + + + + + {{define "FILES"}} + {{range $f := .}} + + + + {{if $f.Service}} + + {{range $d := $f.Service.Dependencies}} + + {{end}} + {{if $f.Service.Delayed}} + + {{end}} + + + {{end}} + + {{end}} + {{end}} + {{template "FILES" .Directory.Files}} + {{define "DIRECTORIES"}} + {{range $d := .}} + + {{template "FILES" $d.Files}} + {{template "DIRECTORIES" $d.Directories}} + + {{end}} + {{end}} + {{template "DIRECTORIES" .Directory.Directories}} + + + + {{range $i, $e := .Environments}} + + + + {{if gt ($e.Condition | len) 0}}{{end}} + + {{end}} + + {{range $i, $r := .Registries}} + + + {{range $j, $v := $r.Values}} + + {{end}} + + {{if gt ($r.Condition | len) 0}}{{end}} + + {{end}} + + + + + + {{if gt (.Icon | len) 0 }} + + {{end}} + + + + + + + + + + + + + + + + + + + + + + + {{range $i, $s := .Shortcuts}} + + + {{if gt ($s.Icon | len) 0}}{{end}} + {{range $j, $p := $s.Properties}}{{end}} + + {{if gt ($s.Condition | len) 0}}{{end}} + + + {{end}} + + + + {{range $i, $h := .Hooks}} + + + {{end}} + + {{range $i, $h := .Hooks}} + + {{if eq $h.When "install"}} + + {{else if eq $h.When "uninstall"}} + + {{else if gt ($h.Condition | len) 0 }} + + {{end}} + + {{end}} + + + + {{range $i, $e := .Environments}} + + {{end}} + {{$id := 0}} + {{define "FILESREF"}} + {{range $f := .}} + + {{end}} + {{end}} + {{template "FILESREF" .Directory.Files}} + {{define "DIRECTORIESREF"}} + {{range $d := .}} + {{template "FILESREF" $d.Files}} + {{template "DIRECTORIESREF" $d.Directories}} + {{end}} + {{end}} + {{template "DIRECTORIESREF" .Directory.Directories}} + {{range $i, $r := .Registries}} + + {{end}} + + {{range $i, $e := .Shortcuts}} + + {{end}} + + + + + + + + + + + + + + + + diff --git a/build/package/msi/wix.json b/build/package/msi/wix.json new file mode 100644 index 0000000..635b3fc --- /dev/null +++ b/build/package/msi/wix.json @@ -0,0 +1,37 @@ +{ + "product": "Draw.io Export", + "company": "MouseSoft", + "license": "tmp\\LICENSE.rtf", + "info": { + "comments": "", + "contact": "Aleksei Badiaev ", + "help-link": "http://mousesoft.ru/help", + "support-telephone": "555-123456789", + "support-link": "http://mousesoft.ru/support", + "update-info-link": "http://mousesoft.ru/update", + "readme": "[INSTALLDIR]readme.txt" + }, + "upgrade-code": "a2313bf4-d6ef-11ed-9abd-071668ee52fd", + "files": [ + { + "path": "out/bin/drawio-export.exe" + } + ], + "shortcuts": [ + { + "name": "MouseSoft Draw.io Export", + "description": "Export Draw.io diagrams using drawio-desktop", + "location": "program", + "target": "[INSTALLDIR]drawio-export.exe", + "wdir": "INSTALLDIR", + "condition": "STARTMENUSHORTCUT ~= \"yes\"" + } + ], + "choco": {}, + "properties": [ + { + "id": "STARTMENUSHORTCUT", + "value": "yes" + } + ] +} \ No newline at end of file diff --git a/makefile b/makefile index f3acc88..efd679e 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,8 @@ # drawio-exporter makefile # ======================== -PROJECT_NAME := drawio-exporter +PROJECT_ID := drawio-export +BIN_SUFFIX := TMPDIR ?= $(CURDIR)/tmp OUTDIR ?= $(CURDIR)/out @@ -28,6 +29,8 @@ MSI_FILE := $(PROJECT_NAME)_$(VERSION)_$(DIST_KIND).msi DIST_EXT := .zip DIST_OPTS := -a -cf ECHO_CMD := echo -e +MSI_VERSION := $(shell echo $(VERSION_NUMBER) | sed -e 's/-.*//') +BIN_SUFFIX := .exe else PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]') DIST_SUFFIX := $(PLATFORM)-$(DIST_KIND) @@ -44,7 +47,7 @@ WHITE := $(shell tput -Txterm setaf 7) CYAN := $(shell tput -Txterm setaf 6) RESET := $(shell tput -Txterm sgr0) -.PHONY: all version version-number test build dist vendor +.PHONY: all version version-number test build dist vendor package pkg-deb version: ## Version of the project to be built @echo $(VERSION) @@ -72,7 +75,7 @@ dist: ## Create all distro packages tar $(DIST_OPTS) "$(OUTDIR)/$(DIST_FILE)" -C "$(OUTDIR)" bin @$(ECHO_CMD) "Dist\t\t${GREEN}[OK]${RESET}" -vendor: ## Copy of all packages needed to support builds and tests in the vendor directory. +vendor: ## Copy of all packages needed to support builds and tests in the vendor directory $(GOCMD) mod vendor @echo "Vendor\t\t${GREEN}[OK]${RESET}" @@ -82,7 +85,56 @@ clean: ## Remove build related files @rm -fr dist/ @$(ECHO_CMD) "Clean\t\t${GREEN}[OK]${RESET}" +## Package: + +ifeq ($(OS),Windows_NT) +PACKAGE_TARGETS := pkg-msi +else +PACKAGE_TARGETS := pkg-deb +endif + +package: $(PACKAGE_TARGETS) ## Build all available packages + @$(ECHO_CMD) "Package\t\t${GREEN}[OK]${RESET}" + +ifeq ($(OS),Windows_NT) +pkg-msi: ## Create MSI package + @rm -f $(OUTDIR)/$(MSI_FILE) + @mkdir -p $(TMPDIR) + go-msi to-rtf --src LICENSE.txt --out $(TMPDIR)/LICENSE.rtf -e + go-msi generate-templates --path build/package/msi/wix.json \ + --src build/package/msi/templates --out $(TMPDIR) \ + --version $(MSI_VERSION) --display $(VERSION) --license tmp/LICENSE.rtf + go-msi gen-wix-cmd --path build/package/msi/wix.json --src tmp \ + --out $(TMPDIR) --arch amd64 --msi $(TMPDIR)/$(MSI_FILE) \ + --version $(MSI_VERSION) --display $(VERSION) + @rm -f wix.dynamic.json + cd $(TMPDIR) && ./build.bat + mv $(TMPDIR)/$(MSI_FILE) $(OUTDIR)/ + @$(ECHO_CMD) "MSI package\t${GREEN}[OK]${RESET}" +else +DEB_NAME := $(PROJECT_ID)_$(VERSION_NUMBER)-1_amd64 + +pkg-deb: ## Build debian package + @mkdir -p $(TMPDIR)/$(DEB_NAME)/usr/bin + @mkdir -p $(TMPDIR)/$(DEB_NAME)/debian + @mkdir -p $(TMPDIR)/$(DEB_NAME)/DEBIAN + @cp -a $(BINDIR)/* $(TMPDIR)/$(DEB_NAME)/usr/bin/ + dpkg-gencontrol -v1.0-1 \ + -c$(CURDIR)/build/package/debian/control \ + -lbuild/package/debian/changelog \ + -f$(TMPDIR)/$(DEB_NAME)/debian/files -Ptmp/$(DEB_NAME) + dpkg-deb --build --root-owner-group $(TMPDIR)/$(DEB_NAME) + dpkg-genchanges --build=binary \ + -c$(CURDIR)/build/package/debian/control \ + -lbuild/package/debian/changelog \ + -f$(TMPDIR)/$(DEB_NAME)/debian/files \ + -u$(TMPDIR) -O$(OUTDIR)/$(DEB_NAME).changes + @mv $(TMPDIR)/*.deb $(OUTDIR)/ + @$(ECHO_CMD) "pkg-deb\t\t${GREEN}[OK]${RESET}" +endif + ## Test: + test: ## Run the tests of the project ifeq ($(EXPORT_RESULT), true) @mkdir -p $(OUTDIR) diff --git a/pkg/drawio/execution.go b/pkg/drawio/execution.go new file mode 100644 index 0000000..54ce958 --- /dev/null +++ b/pkg/drawio/execution.go @@ -0,0 +1,20 @@ +package drawio + +import ( + "errors" + "os/exec" +) + +// Последовательный запуск команд в ОС +func RunSequence(command ...*exec.Cmd) error { + var ( + errs = []error{} + err error + ) + for _, cmd := range command { + if err = cmd.Run(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} diff --git a/pkg/drawio/export.go b/pkg/drawio/export.go index bf3e699..9a98c66 100644 --- a/pkg/drawio/export.go +++ b/pkg/drawio/export.go @@ -3,7 +3,6 @@ package drawio import ( "bufio" "errors" - "fmt" "io" "io/fs" "os" @@ -30,39 +29,141 @@ func New(opt ...Option) Exporter { // Создать нового экспортёра с параметрами func NewWithOptions(opts *Options) Exporter { - return Exporter{opts: opts} + exp := Exporter{opts: opts} + if opts.openFile == nil { + exp.openFile = func(name string) (io.ReadCloser, error) { + return os.Open(name) + } + } else { + exp.openFile = opts.openFile + } + if opts.readDir == nil { + exp.readDir = func(name string) ([]os.DirEntry, error) { + return os.ReadDir(name) + } + } else { + exp.readDir = opts.readDir + } + return exp } // Экспортёр диаграмм type Exporter struct { - opts *Options + opts *Options // Параметры экспортёра диаграмм + openFile OpenFileFunc // Открыть файл + readDir ReadDirFunc // Прочитать папку на диске } // Экспорт диаграмм из указанный файлов или папок func (exp Exporter) Export(fileOrDir ...string) error { var ( - err error - errs = []error{} - args = exp.opts.Args() - info fs.FileInfo + commands = []*exec.Cmd{} + err error + errs = []error{} + args = exp.opts.Args() + info fs.FileInfo ) for _, item := range fileOrDir { if info, err = os.Stat(item); err != nil { errs = append(errs, err) continue } + var addCommands []*exec.Cmd if info.IsDir() { - err = exp.exportDir(item, args) + addCommands, err = exp.ExportDir(item, args) } else { - err = exp.exportFile(item, args) + addCommands, err = exp.ExportFile(item, args) } + commands = append(commands, addCommands...) if err != nil { errs = append(errs, err) } } + if err = RunSequence(commands...); err != nil { + errs = append(errs, err) + } return errors.Join(errs...) } +// Экспорт диаграмм из всех файлов в папке +func (exp Exporter) ExportDir( + dirPath string, args []string, subDir ...string, +) ([]*exec.Cmd, error) { + var ( + commands = []*exec.Cmd{} + err error + entries []fs.DirEntry + errs []error + ) + if entries, err = exp.readDir(dirPath); err != nil { + return commands, err + } + for _, entry := range entries { + name := entry.Name() + var addCommands []*exec.Cmd + if entry.IsDir() { + if exp.opts.Recursive { + deep := len(subDir) + outDir := make([]string, deep+1) + copy(outDir, subDir) + outDir[deep] = name + addCommands, err = exp.ExportDir(path.Join(dirPath, name), args, outDir...) + } else { + continue + } + } else { + addCommands, err = exp.ExportFile(path.Join(dirPath, name), args, subDir...) + } + commands = append(commands, addCommands...) + if err != nil { + errs = append(errs, err) + } + } + return commands, errors.Join(errs...) +} + +// Экспорт файла с диаграммами +func (exp Exporter) ExportFile( + filePath string, args []string, subDir ...string, +) ([]*exec.Cmd, error) { + var ( + commands = []*exec.Cmd{} + err error + file io.ReadCloser + diagrams []string + ) + if file, err = exp.openFile(filePath); err != nil { + return commands, err + } + defer file.Close() + if diagrams, err = Diagrams(file); err != nil { + return commands, err + } + outDirs := make([]string, 1, len(subDir)+1) + outDirs[0] = exp.opts.OutDir() + outDirs = append(outDirs, subDir...) + output := path.Join(outDirs...) + for i, name := range diagrams { + outName := strings.TrimSuffix(path.Base(filePath), path.Ext(filePath)) + if !exp.opts.RemovePageSuffix || len(diagrams) > 1 { + outName += "-" + name + } + outName += exp.opts.OutExt() + drawioArgs := make([]string, len(args), len(args)+4) + copy(drawioArgs, args) + drawioArgs = append(drawioArgs, + "--page-index", strconv.Itoa(i), + "--output", path.Join(output, outName), + "--export", filePath, + ) + cmd := exec.Command(exp.opts.App(), drawioArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + commands = append(commands, cmd) + } + return commands, err +} + // Разбирает данные и возвращает срез имён диаграмм в них func Diagrams(reader io.Reader) ([]string, error) { var ( @@ -84,72 +185,3 @@ func Diagrams(reader io.Reader) ([]string, error) { } return result, nil } - -// Экспорт диаграмм из всех файлов в папке -func (exp Exporter) exportDir(dirPath string, args []string, subDir ...string) error { - var ( - entries []fs.DirEntry - err error - errs []error - ) - if entries, err = os.ReadDir(dirPath); err != nil { - return err - } - for _, entry := range entries { - name := entry.Name() - outDir := make([]string, len(subDir), len(subDir)+1) - outDir = append(outDir, name) - if entry.IsDir() && exp.opts.Recursive { - err = exp.exportDir(path.Join(dirPath, name), args, outDir...) - } else { - err = exp.exportFile(path.Join(dirPath, name), args, outDir...) - } - if err != nil { - errs = append(errs, err) - } - } - return errors.Join(errs...) -} - -// Экспорт файла с диаграммами -func (exp Exporter) exportFile(filePath string, args []string, subDir ...string) error { - var ( - file *os.File - diagrams []string - err error - ) - if file, err = os.Open(filePath); err != nil { - return err - } - defer file.Close() - if diagrams, err = Diagrams(file); err != nil { - return err - } - outDirs := make([]string, 1, len(subDir)+1) - outDirs[0] = exp.opts.Output - outDirs = append(outDirs, subDir...) - output := path.Join(outDirs...) - errs := []error{} - for i, name := range diagrams { - outName := strings.TrimSuffix(path.Base(filePath), path.Ext(filePath)) - if !exp.opts.RemovePageSuffix || len(diagrams) > 1 { - outName += "-" + name - } - outName += exp.opts.OutExt() - drawioArgs := make([]string, len(args), len(args)+4) - copy(drawioArgs, args) - drawioArgs = append(drawioArgs, - "--page-index", strconv.Itoa(i), - "--output", path.Join(output, outName), - "--export", filePath, - ) - cmd := exec.Command(exp.opts.App(), drawioArgs...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - fmt.Println("Run command:", cmd.Path, cmd.Args) - if err = cmd.Run(); err != nil { - errs = append(errs, err) - } - } - return errors.Join(errs...) -} diff --git a/pkg/drawio/export_test.go b/pkg/drawio/export_test.go index 55f66b7..8cb4b44 100644 --- a/pkg/drawio/export_test.go +++ b/pkg/drawio/export_test.go @@ -1,22 +1,126 @@ package drawio_test import ( + "io" + "io/fs" + "os" + "os/exec" + "path" "strings" "testing" + "time" "git.mousesoft.ru/ms/drawio-exporter/pkg/drawio" "github.com/stretchr/testify/assert" ) +// Команда оболочки ОС +type command struct { + cmd string // Команда + args []string // Аргументы команды +} + +// Файл с диаграммами +type source struct { + output string // Папка вывода для ExportFile + data string // Данные файла с диаграммами + diagrams []string // Срез имён диаграмм + commands []command // Ожидаемые команды экспорта диаграмм +} + +// Элемент папки с файлами +type dirEntry struct { + name string // Имя элемента + isDir bool // Истина, если элемент является папкой +} + +var ( + _ os.DirEntry = (*dirEntry)(nil) + _ os.FileInfo = (*dirEntry)(nil) +) + +func (entry dirEntry) Name() string { + return entry.name +} + +func (entry dirEntry) IsDir() bool { + return entry.isDir +} + +func (entry dirEntry) Type() fs.FileMode { + if entry.isDir { + return fs.ModeDir + } + return 0 +} + +func (entry dirEntry) Info() (fs.FileInfo, error) { + return nil, nil +} + +func (entry dirEntry) Size() int64 { + return 100 +} + +func (entry dirEntry) Mode() fs.FileMode { + return entry.Type() +} + +func (entry dirEntry) ModTime() time.Time { + return time.Now() +} + +func (entry dirEntry) Sys() any { + return nil +} + +// Конвертация команд в информацию о командах +func cmd2info(commands []*exec.Cmd) []command { + cmdInfo := make([]command, len(commands)) + for i, cmd := range commands { + cmdInfo[i] = command{ + cmd: cmd.Path, + args: make([]string, len(cmd.Args)), + } + copy(cmdInfo[i].args, cmd.Args) + } + return cmdInfo +} + +type exportTest struct { + name string // Наименование теста + dirs map[string][]os.DirEntry // Папки файлов с диаграммами + files map[string]source // Файлы с диаграммами +} + +// Информация об ожидаемых командах при экспорте папки +func (test exportTest) dirCommands(dir string, recursive bool) []command { + commands := make([]command, 0, 4) + if tc, ok := test.dirs[dir]; ok { + for _, entry := range tc { + path := path.Join(dir, entry.Name()) + if entry.IsDir() && recursive { + commands = append(commands, test.dirCommands(path, true)...) + } else { + commands = append(commands, test.files[path].commands...) + } + } + } + return commands +} + // Тестовые данные -var testData = []struct { - name string // Наименование теста - source string // Данные файла с диаграммами - diagrams []string // Срез имён диаграмм -}{ +var testData = []exportTest{ { name: "positive case", - source: ` + dirs: map[string][]os.DirEntry{ + "source": { + dirEntry{name: "diagrams.drawio", isDir: false}, + }, + }, + files: map[string]source{ + "source/diagrams.drawio": { + data: ` @@ -30,11 +134,78 @@ var testData = []struct { `, - diagrams: []string{"1", "2", "3"}, + diagrams: []string{"1", "2", "3"}, + commands: []command{ + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "0", + "--output", "export/diagrams-1.pdf", + "--export", "source/diagrams.drawio", + }, + }, + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "1", + "--output", "export/diagrams-2.pdf", + "--export", "source/diagrams.drawio", + }, + }, + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "2", + "--output", "export/diagrams-3.pdf", + "--export", "source/diagrams.drawio", + }, + }, + }, + }, + }, }, { name: "invalid source", - source: ` + dirs: map[string][]os.DirEntry{ + "source": { + dirEntry{name: "diagrams.drawio", isDir: false}, + }, + }, + files: map[string]source{ + "source/diagrams.drawio": { + data: ` + + + + + + + + + + + + +`, + diagrams: []string{}, + }, + }, + }, + { + name: "nested dirs", + dirs: map[string][]os.DirEntry{ + "source": { + dirEntry{name: "diagrams.drawio", isDir: false}, + dirEntry{name: "subdir", isDir: true}, + dirEntry{name: "additional.xml", isDir: false}, + }, + "source/subdir": { + dirEntry{name: "Вложенные диаграммы.drawio", isDir: false}, + }, + }, + files: map[string]source{ + "source/diagrams.drawio": { + data: ` @@ -47,17 +218,193 @@ var testData = []struct { -`, - diagrams: []string{}, +`, + diagrams: []string{"1", "2", "3"}, + commands: []command{ + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "0", + "--output", "export/diagrams-1.pdf", + "--export", "source/diagrams.drawio", + }, + }, + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "1", + "--output", "export/diagrams-2.pdf", + "--export", "source/diagrams.drawio", + }, + }, + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "2", + "--output", "export/diagrams-3.pdf", + "--export", "source/diagrams.drawio", + }, + }, + }, + }, + "source/additional.xml": { + data: ` + + + + + + + + +`, + diagrams: []string{"Один", "Два"}, + commands: []command{ + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "0", + "--output", "export/additional-Один.pdf", + "--export", "source/additional.xml", + }, + }, + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "1", + "--output", "export/additional-Два.pdf", + "--export", "source/additional.xml", + }, + }, + }, + }, + "source/subdir/Вложенные диаграммы.drawio": { + output: "export/subdir", + data: ` + + + + + + + + +`, + diagrams: []string{"Первая диаграмма", "Вторая диаграмма"}, + commands: []command{ + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "0", + "--output", "export/subdir/Вложенные диаграммы-Первая диаграмма.pdf", + "--export", "source/subdir/Вложенные диаграммы.drawio", + }, + }, + { + cmd: "/usr/bin/drawio", + args: []string{ + "drawio", "--page-index", "1", + "--output", "export/subdir/Вложенные диаграммы-Вторая диаграмма.pdf", + "--export", "source/subdir/Вложенные диаграммы.drawio", + }, + }, + }, + }, + }, }, } func TestDiagrams(t *testing.T) { for _, test := range testData { t.Run(test.name, func(t *testing.T) { - diagrams, err := drawio.Diagrams(strings.NewReader(test.source)) - assert.NoError(t, err) - assert.ElementsMatch(t, test.diagrams, diagrams) + for filePath, source := range test.files { + t.Run(filePath, func(t *testing.T) { + diagrams, err := drawio.Diagrams(strings.NewReader(source.data)) + assert.NoError(t, err) + assert.ElementsMatch(t, source.diagrams, diagrams) + }) + } + }) + } +} + +func TestExportFile(t *testing.T) { + for _, test := range testData { + t.Run(test.name, func(t *testing.T) { + for filePath, source := range test.files { + var openOpt = drawio.WithNop() + if len(source.output) > 0 { + openOpt = drawio.WithOutput(source.output) + } + t.Run(filePath, func(t *testing.T) { + var ( + openFileCalls = []string{} + exp = drawio.New( + drawio.WithOpenFile(func(s string) (io.ReadCloser, error) { + openFileCalls = append(openFileCalls, s) + return io.NopCloser(strings.NewReader(source.data)), nil + }), + openOpt, + ) + ) + commands, err := exp.ExportFile(filePath, []string{}) + assert.NoError(t, err) + if assert.Equal(t, 1, len(openFileCalls)) { + assert.Equal(t, filePath, openFileCalls[0]) + } + assert.ElementsMatch(t, source.commands, cmd2info(commands)) + }) + } + }) + } +} + +func TestExportDir(t *testing.T) { + for _, recursive := range []bool{false, true} { + var ( + name string + recursiveOption drawio.Option + ) + if recursive { + name = "recursive" + recursiveOption = drawio.WithRecursive() + } else { + name = "non-recursive" + recursiveOption = drawio.WithNop() + } + t.Run(name, func(t *testing.T) { + for _, test := range testData { + t.Run(test.name, func(t *testing.T) { + var ( + openFileCalls = []string{} + exp = drawio.New( + drawio.WithOpenFile(func(s string) (io.ReadCloser, error) { + openFileCalls = append(openFileCalls, s) + return io.NopCloser( + strings.NewReader(test.files[s].data), + ), nil + }), + drawio.WithReadDir(func(s string) ([]os.DirEntry, error) { + return test.dirs[s], nil + }), + recursiveOption, + ) + ) + for dir := range test.dirs { + pathDir, _ := path.Split(dir) + if len(pathDir) > 0 { + continue + } + t.Run(dir, func(t *testing.T) { + openFileCalls = openFileCalls[:0] + commands, err := exp.ExportDir(dir, []string{}) + assert.NoError(t, err) + assert.ElementsMatch(t, + test.dirCommands(dir, recursive), cmd2info(commands)) + }) + } + }) + } }) } } diff --git a/pkg/drawio/options.go b/pkg/drawio/options.go index 2249cf0..e9e9c58 100644 --- a/pkg/drawio/options.go +++ b/pkg/drawio/options.go @@ -3,6 +3,8 @@ package drawio import ( "flag" "fmt" + "io" + "os" "strconv" ) @@ -21,10 +23,28 @@ func (e ErrUnsupportedFormat) Error() string { return fmt.Sprintf("unsupported format: '%s'", e.Format) } +// Проверка эквивалентности ошибок +func (e ErrUnsupportedFormat) Is(target error) bool { + switch t := target.(type) { + case ErrUnsupportedFormat: + return t.Format == e.Format + } + return false +} + var _ error = (*ErrUnsupportedFormat)(nil) // Проверка реализации интерфейса type Format string // Формат экспортированного файла +const ( + PDF Format = "pdf" + PNG Format = "png" + JPG Format = "jpg" + SVG Format = "svg" + VSDX Format = "vsdx" + XML Format = "xml" +) + // Строковое представление формата func (f Format) String() string { return string(f) @@ -41,6 +61,11 @@ func (f *Format) Set(str string) error { return &ErrUnsupportedFormat{str} } +// Список всех поддерживаемых форматов экспорта +func SupportedFormats() []Format { + return []Format{"pdf", "png", "jpg", "svg", "vsdx", "xml"} +} + var _ flag.Value = (*Format)(nil) // Поверка реализации интерфейса // Расширение файла заданного формата @@ -59,16 +84,24 @@ type Options struct { Transparent bool // Прозрачный фона для PNG // Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG EmbedDiagram bool - EmbedSvgImages bool // Встраивать изображения в файл формата SVG - Border uint // Ширина рамки вокруг диаграмм - Scale uint // Масштаб в процентах размера экспортированных диаграмм - Width uint // Ширина экспортированной диаграммы с сохранением масштаба - Height uint // Высота экспортированной диаграммы с сохранением масштаба - Crop bool // Обрезать результирующий PDF до размера диаграммы - Uncompressed bool // Выводить несжатый XML - EnablePlugins bool // Включить подключаемые модули + EmbedSvgImages bool // Встраивать изображения в файл формата SVG + Border uint // Ширина рамки вокруг диаграмм + Scale uint // Масштаб в процентах размера экспортированных диаграмм + Width uint // Ширина экспортированной диаграммы с сохранением масштаба + Height uint // Высота экспортированной диаграммы с сохранением масштаба + Crop bool // Обрезать результирующий PDF до размера диаграммы + Uncompressed bool // Выводить несжатый XML + EnablePlugins bool // Включить подключаемые модули + openFile OpenFileFunc // Открыть файл + readDir ReadDirFunc // Прочитать папку на диске } +// Функция открытия файла +type OpenFileFunc func(string) (io.ReadCloser, error) + +// Функция чтения папки на диске +type ReadDirFunc func(string) ([]os.DirEntry, error) + // Путь к приложению drawio func (opts Options) App() string { app := opts.Application @@ -96,7 +129,7 @@ func (opts Options) OutExt() string { return fmt.ext() } -// Формирование аргументов командной строки +// Аргументы командной строки drawio func (opts Options) Args() []string { args := []string{} if len(opts.Format) > 0 { @@ -202,7 +235,7 @@ func (opt optionRemovePageSuffix) apply(opts *Options) { } // Качество экспортированного изображения (только для JPEG) -func WithQuality(q int) Option { +func WithQuality(q uint) Option { return optionQuality(q) } @@ -254,7 +287,7 @@ func (opt optionEmbedSvgImages) apply(opts *Options) { } // Ширина рамки вокруг диаграмм -func WithBorder(border int) Option { +func WithBorder(border uint) Option { return optionBorder(border) } @@ -267,7 +300,7 @@ func (opt optionBorder) apply(opts *Options) { } // Масштаб в процентах размера экспортированных диаграмм -func WithScale(scale int) Option { +func WithScale(scale uint) Option { return optionScale(scale) } @@ -280,7 +313,7 @@ func (opt optionScale) apply(opts *Options) { } // Ширина экспортированной диаграммы с сохранением масштаба -func WithWidth(width int) Option { +func WithWidth(width uint) Option { return optionWidth(width) } @@ -293,7 +326,7 @@ func (opt optionWidth) apply(opts *Options) { } // Высота экспортированной диаграммы с сохранением масштаба -func WithHeight(height int) Option { +func WithHeight(height uint) Option { return optionHeight(height) } @@ -344,7 +377,45 @@ func (opt optionEnablePlugins) apply(opts *Options) { opts.EnablePlugins = true } -// Список всех поддерживаемых форматов экспорта -func SupportedFormats() []Format { - return []Format{"pdf", "png", "jpg", "svg", "vsdx", "xml"} +// Открыть файл +func WithOpenFile(openFile OpenFileFunc) Option { + return optionOpenFile{openFile} +} + +type optionOpenFile struct { + openFile OpenFileFunc +} + +var _ Option = optionOpenFile{} // Проверка реализации интерфейса + +func (opt optionOpenFile) apply(opts *Options) { + opts.openFile = opt.openFile +} + +// Прочитать папку на диске +func WithReadDir(readDir ReadDirFunc) Option { + return optionReadDir{readDir} +} + +type optionReadDir struct { + readDir ReadDirFunc +} + +var _ Option = optionReadDir{} // Проверка реализации интерфейса + +func (opt optionReadDir) apply(opts *Options) { + opts.readDir = opt.readDir +} + +// Пустой параметр +func WithNop() Option { + return optionNop{} +} + +type optionNop struct{} + +var _ Option = optionNop{} + +func (opt optionNop) apply(opts *Options) { + // Не требуется действий, так как это пустой параметр } diff --git a/pkg/drawio/options_internal_test.go b/pkg/drawio/options_internal_test.go new file mode 100644 index 0000000..c376f07 --- /dev/null +++ b/pkg/drawio/options_internal_test.go @@ -0,0 +1,105 @@ +package drawio + +import ( + "io" + "os" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptions(t *testing.T) { + testData := []struct { + name string // Наименование теста + opts []Option // Параметры + app string // Путь к приложению drawio + outdir string // Путь к папке с экспортированными файлами + outext string // Расширение экспортированных файлов + args []string // Аргументы командной строки drawio + }{ + { + name: "default", + opts: []Option{}, + app: "drawio", + outdir: "export", + outext: ".pdf", + args: []string{}, + }, + { + name: "svg with all options", + opts: []Option{ + WithAppPath("/usr/local/bin/drawio"), + WithOutput("/tmp/images/"), + WithFormat(SVG), + WithRecursive(), + WithRemovePageSuffix(), + WithQuality(100), + WithTransparent(), + WithEmbedDiagram(), + WithEmbedSvgImages(), + WithBorder(1), + WithScale(120), + WithWidth(800), + WithHeight(600), + WithCrop(), + WithUncompressed(), + WithEnablePlugins(), + }, + app: "/usr/local/bin/drawio", + outdir: "/tmp/images/", + outext: ".svg", + args: []string{ + "--format", "svg", + "--quality", "100", + "--transparent", + "--embed-diagram", + "--embed-svg-images", + "--border", "1", + "--scale", "120", + "--width", "800", + "--height", "600", + "--crop", + "--uncompressed", + "--enable-plugins", + }, + }, + } + for _, test := range testData { + t.Run(test.name, func(t *testing.T) { + options := Options{} + for _, opt := range test.opts { + opt.apply(&options) + } + assert.Equal(t, test.app, options.App()) + assert.Equal(t, test.outdir, options.OutDir()) + assert.Equal(t, test.outext, options.OutExt()) + assert.ElementsMatch(t, test.args, options.Args()) + }) + } +} + +// Тестовая функция открытия файла +func openFile(name string) (io.ReadCloser, error) { + return nil, nil +} + +func TestOptionsOpenFileFunc(t *testing.T) { + opts := New(WithOpenFile(openFile)) + assert.Equal(t, + reflect.ValueOf(openFile).Pointer(), + reflect.ValueOf(opts.openFile).Pointer(), + ) +} + +func readDir(name string) ([]os.DirEntry, error) { + return []os.DirEntry{}, nil +} + +func TestOptionsReadDirFunc(t *testing.T) { + opts := New(WithReadDir(readDir)) + assert.Equal(t, + reflect.ValueOf(readDir).Pointer(), + reflect.ValueOf(opts.readDir).Pointer(), + ) +} diff --git a/pkg/drawio/options_test.go b/pkg/drawio/options_test.go new file mode 100644 index 0000000..161309d --- /dev/null +++ b/pkg/drawio/options_test.go @@ -0,0 +1,76 @@ +package drawio_test + +import ( + "testing" + + "git.mousesoft.ru/ms/drawio-exporter/pkg/drawio" + "github.com/stretchr/testify/assert" +) + +func TestFormat(t *testing.T) { + testData := []struct { + name string + err error + format drawio.Format + ext string + }{ + { + name: "pdf", + err: nil, + format: drawio.PDF, + ext: ".pdf", + }, + { + name: "png", + err: nil, + format: drawio.PNG, + ext: ".png", + }, + { + name: "jpg", + err: nil, + format: drawio.JPG, + ext: ".jpg", + }, + { + name: "svg", + err: nil, + format: drawio.SVG, + ext: ".svg", + }, + { + name: "vsdx", + err: nil, + format: drawio.VSDX, + ext: ".vsdx", + }, + { + name: "xml", + err: nil, + format: drawio.XML, + ext: ".xml", + }, + { + name: "svvg", + err: drawio.ErrUnsupportedFormat{"svvg"}, + format: drawio.Format(""), + ext: "", + }, + } + for _, test := range testData { + t.Run(test.name, func(t *testing.T) { + var ( + v drawio.Format + err error + ) + err = (&v).Set(test.name) + if test.err == nil { + assert.Equal(t, test.name, test.format.String()) + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, test.err) + assert.ErrorContains(t, err, test.err.Error()) + } + }) + } +}