diff --git a/cmd/commandline/plugin/package.go b/cmd/commandline/plugin/package.go index 04f7d02..6dba611 100644 --- a/cmd/commandline/plugin/package.go +++ b/cmd/commandline/plugin/package.go @@ -8,6 +8,10 @@ import ( "github.com/langgenius/dify-plugin-daemon/internal/utils/log" ) +var ( + MaxPluginPackageSize = int64(52428800) // 50MB +) + func PackagePlugin(inputPath string, outputPath string) { decoder, err := decoder.NewFSPluginDecoder(inputPath) if err != nil { @@ -17,7 +21,7 @@ func PackagePlugin(inputPath string, outputPath string) { } packager := packager.NewPackager(decoder) - zipFile, err := packager.Pack() + zipFile, err := packager.Pack(MaxPluginPackageSize) if err != nil { log.Error("failed to package plugin %v", err) diff --git a/cmd/commandline/plugin/python.go b/cmd/commandline/plugin/python.go index 85dfcd7..56fb45d 100644 --- a/cmd/commandline/plugin/python.go +++ b/cmd/commandline/plugin/python.go @@ -94,6 +94,12 @@ var PYTHON_AGENT_STRATEGY_TEMPLATE []byte //go:embed templates/python/GUIDE.md var PYTHON_GUIDE []byte +//go:embed templates/python/.difyignore +var PYTHON_DIFYIGNORE []byte + +//go:embed templates/python/.gitignore +var PYTHON_GITIGNORE []byte + func renderTemplate( original_template []byte, manifest *plugin_entities.PluginDeclaration, supported_model_types []string, ) (string, error) { @@ -149,6 +155,14 @@ func createPythonEnvironment( return err } + if err := writeFile(filepath.Join(root, ".difyignore"), string(PYTHON_DIFYIGNORE)); err != nil { + return err + } + + if err := writeFile(filepath.Join(root, ".gitignore"), string(PYTHON_GITIGNORE)); err != nil { + return err + } + if category == "tool" { if err := createPythonTool(root, manifest); err != nil { return err diff --git a/cmd/commandline/plugin/templates/python/.difyignore b/cmd/commandline/plugin/templates/python/.difyignore new file mode 100644 index 0000000..3e398ba --- /dev/null +++ b/cmd/commandline/plugin/templates/python/.difyignore @@ -0,0 +1,165 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ \ No newline at end of file diff --git a/cmd/commandline/plugin/templates/python/.gitignore b/cmd/commandline/plugin/templates/python/.gitignore new file mode 100644 index 0000000..c2fb773 --- /dev/null +++ b/cmd/commandline/plugin/templates/python/.gitignore @@ -0,0 +1,168 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/cmd/packager/main.go b/cmd/packager/main.go deleted file mode 100644 index 6881310..0000000 --- a/cmd/packager/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "flag" - "os" - - "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_packager/decoder" - "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_packager/packager" - "github.com/langgenius/dify-plugin-daemon/internal/utils/log" -) - -func main() { - var ( - in_path string - out_path string - help bool - ) - - flag.StringVar(&in_path, "in", "", "directory of plugin to be packaged") - flag.StringVar(&out_path, "out", "", "package output path") - flag.BoolVar(&help, "help", false, "show help") - flag.Parse() - - if help || in_path == "" || out_path == "" { - flag.PrintDefaults() - os.Exit(0) - } - - decoder, err := decoder.NewFSPluginDecoder(in_path) - if err != nil { - log.Panic("failed to create plugin decoder , plugin path: %s, error: %v", in_path, err) - } - - packager := packager.NewPackager(decoder) - zipFile, err := packager.Pack() - - if err != nil { - log.Panic("failed to package plugin %v", err) - } - - err = os.WriteFile(out_path, zipFile, 0644) - if err != nil { - log.Panic("failed to write package file %v", err) - } - - log.Info("plugin packaged successfully, output path: %s", out_path) -} diff --git a/internal/core/bundle_packager/zip.go b/internal/core/bundle_packager/zip.go index b06cd87..0aa1e9e 100644 --- a/internal/core/bundle_packager/zip.go +++ b/internal/core/bundle_packager/zip.go @@ -2,8 +2,12 @@ package bundle_packager import ( "archive/zip" + "bytes" + "errors" "io" "os" + "strconv" + "strings" ) type ZipBundlePackager struct { @@ -37,6 +41,47 @@ func NewZipBundlePackager(path string) (BundlePackager, error) { return zipBundlePackager, nil } +func NewZipBundlePackagerWithSizeLimit(path string, maxSize int64) (BundlePackager, error) { + zipFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer zipFile.Close() + + zipBytes, err := io.ReadAll(zipFile) + if err != nil { + return nil, err + } + + reader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) + if err != nil { + return nil, errors.New(strings.ReplaceAll(err.Error(), "zip", "difypkg")) + } + + totalSize := int64(0) + for _, file := range reader.File { + totalSize += int64(file.UncompressedSize64) + if totalSize > maxSize { + return nil, errors.New( + "bundle package size is too large, please ensure the uncompressed size is less than " + + strconv.FormatInt(maxSize, 10) + " bytes", + ) + } + } + + memoryPackager, err := NewMemoryZipBundlePackager(zipBytes) + if err != nil { + return nil, err + } + + zipBundlePackager := &ZipBundlePackager{ + MemoryZipBundlePackager: memoryPackager, + path: path, + } + + return zipBundlePackager, nil +} + func (p *ZipBundlePackager) Save() error { // export the bundle to a zip file zipBytes, err := p.Export() diff --git a/internal/core/plugin_manager/serverless/packager.go b/internal/core/plugin_manager/serverless/packager.go index 64eaeda..65a9633 100644 --- a/internal/core/plugin_manager/serverless/packager.go +++ b/internal/core/plugin_manager/serverless/packager.go @@ -4,7 +4,6 @@ import ( "archive/tar" "compress/gzip" "errors" - "fmt" "io" "io/fs" "os" @@ -100,11 +99,6 @@ func (p *Packager) Pack() (*os.File, error) { return err } - if state.Size() > 1024*1024*10 { - // 10MB, 1 single file is too large - return fmt.Errorf("file size is too large: %s, max 10MB", fullFilename) - } - tarHeader, err := tar.FileInfoHeader(state, fullFilename) if err != nil { return err diff --git a/internal/core/plugin_packager/decoder/zip.go b/internal/core/plugin_packager/decoder/zip.go index 793bf7c..a2577c6 100644 --- a/internal/core/plugin_packager/decoder/zip.go +++ b/internal/core/plugin_packager/decoder/zip.go @@ -10,6 +10,7 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" "github.com/langgenius/dify-plugin-daemon/internal/types/entities/plugin_entities" @@ -50,6 +51,28 @@ func NewZipPluginDecoder(binary []byte) (*ZipPluginDecoder, error) { return decoder, nil } +// NewZipPluginDecoderWithSizeLimit is a helper function to create a ZipPluginDecoder with a size limit +// It checks the total uncompressed size of the plugin package and returns an error if it exceeds the max size +func NewZipPluginDecoderWithSizeLimit(binary []byte, maxSize int64) (*ZipPluginDecoder, error) { + reader, err := zip.NewReader(bytes.NewReader(binary), int64(len(binary))) + if err != nil { + return nil, errors.New(strings.ReplaceAll(err.Error(), "zip", "difypkg")) + } + + totalSize := int64(0) + for _, file := range reader.File { + totalSize += int64(file.UncompressedSize64) + if totalSize > maxSize { + return nil, errors.New( + "plugin package size is too large, please ensure the uncompressed size is less than " + + strconv.FormatInt(maxSize, 10) + " bytes", + ) + } + } + + return NewZipPluginDecoder(binary) +} + func (z *ZipPluginDecoder) Stat(filename string) (fs.FileInfo, error) { f, err := z.reader.Open(filename) if err != nil { diff --git a/internal/core/plugin_packager/packager/packager.go b/internal/core/plugin_packager/packager/packager.go index 0d1b120..3d87b03 100644 --- a/internal/core/plugin_packager/packager/packager.go +++ b/internal/core/plugin_packager/packager/packager.go @@ -3,7 +3,9 @@ package packager import ( "archive/zip" "bytes" + "errors" "path/filepath" + "strconv" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_packager/decoder" ) @@ -20,7 +22,7 @@ func NewPackager(decoder decoder.PluginDecoder) *Packager { } } -func (p *Packager) Pack() ([]byte, error) { +func (p *Packager) Pack(maxSize int64) ([]byte, error) { err := p.Validate() if err != nil { return nil, err @@ -29,6 +31,8 @@ func (p *Packager) Pack() ([]byte, error) { zipBuffer := new(bytes.Buffer) zipWriter := zip.NewWriter(zipBuffer) + totalSize := int64(0) + err = p.decoder.Walk(func(filename, dir string) error { fullPath := filepath.Join(dir, filename) file, err := p.decoder.ReadFile(fullPath) @@ -36,6 +40,11 @@ func (p *Packager) Pack() ([]byte, error) { return err } + totalSize += int64(len(file)) + if totalSize > maxSize { + return errors.New("plugin package size is too large, please ensure the uncompressed size is less than " + strconv.FormatInt(maxSize, 10) + " bytes") + } + zipFile, err := zipWriter.Create(fullPath) if err != nil { return err diff --git a/internal/core/plugin_packager/packager_test.go b/internal/core/plugin_packager/packager_test.go index eb19693..6e2b5cd 100644 --- a/internal/core/plugin_packager/packager_test.go +++ b/internal/core/plugin_packager/packager_test.go @@ -120,7 +120,7 @@ func TestPackagerAndVerifier(t *testing.T) { packager := packager.NewPackager(originDecoder) // pack - zip, err := packager.Pack() + zip, err := packager.Pack(52428800) if err != nil { t.Errorf("failed to pack: %s", err.Error()) return @@ -202,7 +202,7 @@ func TestWrongSign(t *testing.T) { packager := packager.NewPackager(originDecoder) // pack - zip, err := packager.Pack() + zip, err := packager.Pack(52428800) if err != nil { t.Errorf("failed to pack: %s", err.Error()) return diff --git a/internal/service/plugin_decoder.go b/internal/service/plugin_decoder.go index 2a20e66..6ebe511 100644 --- a/internal/service/plugin_decoder.go +++ b/internal/service/plugin_decoder.go @@ -29,7 +29,7 @@ func UploadPluginPkg( return exception.InternalServerError(err).ToResponse() } - decoder, err := decoder.NewZipPluginDecoder(pluginFile) + decoder, err := decoder.NewZipPluginDecoderWithSizeLimit(pluginFile, config.MaxPluginPackageSize) if err != nil { return exception.BadRequestError(err).ToResponse() }