背景
公司使用内外网隔离场景下,每次更新 IDEs 还要同时更新插件是很麻烦的。本人同时使用 Goland、PhpStorm、PyCharm(为啥不是装一个加其它语言插件?我不造啊),然后每个 IDEs 都要更新十几个插件,还不能批量更新,气死了!但人总是想偷懒的,不能偷懒那跟咸鱼有什么区别呢!动动手搞一个可以在线更新即可
原理
-
自定义插件存储库 (ps 我说这个是当初始皇的插件发现的吗 zhili.io/updatePlugins.xml)
-
根据插件 xmlid 查找到对应的下载地址,把相关的 jar、.zip、.blockmap.zip、.hash.json 以前下,然后组装
updatePlugins.xml
-
在 IDE 中 填写
http://127.0.0.1/updatePlugins.xml
即可
代码
了解原理,就可以先这样,然后这样,最后这样就可以了,不对,还是直接上代码吧
package main
import (
"bufio"
"context"
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"github.com/google/go-querystring/query"
"github.com/spf13/afero"
)
type (
Loader interface {
Load() ([]string, error)
}
Downloader interface {
Download(ctx context.Context, id string) (*IdeaPlugin, error)
}
Exporter interface {
Export(filename string, data any) error
}
)
type txtLoader struct {
fs afero.Fs
confPath string
}
func (t *txtLoader) Load() ([]string, error) {
fd, err := os.Open(t.confPath)
if err != nil {
return nil, err
}
ids := make([]string, 0)
reader := bufio.NewReader(fd)
for {
line, err := reader.ReadString('\n')
if len(line) == 0 && err == io.EOF {
break
}
if line == "" || !strings.HasPrefix(line, "http") {
log.Println("read file ignore :", line)
continue
}
parse, err := url.Parse(strings.Trim(line, "\r\n\t"))
if err != nil {
log.Printf("err for url %serror %s\n", line, err)
continue
}
if parse.Path != "/plugin/index" {
ids = append(ids, strings.ReplaceAll(parse.Path, "/plugin/", ""))
continue
}
if v := parse.Query().Get("xmlId"); v != "" {
ids = append(ids, v)
}
}
return ids, nil
}
func NewTxtLoader(fs afero.Fs, configPath string) Loader {
return &txtLoader{fs: fs, confPath: configPath}
}
type (
PluginRepository struct {
XMLName xml.Name `xml:"plugin-repository"`
FF string `xml:"ff"`
Category Category `xml:"category"`
}
Category struct {
Name string `xml:"name,attr"`
IdeaPlugin []IdeaPlugin `xml:"idea-plugin"`
}
IdeaVersion struct {
Min string `xml:"min,attr"`
Max string `xml:"max,attr"`
SinceBuild string `xml:"since-build,attr"`
UntilBuild string `xml:"until-build,attr"`
}
ChangeNotes struct {
ChangeNotes string `xml:",cdata"`
}
IdeaPlugin struct {
Downloads string `xml:"downloads,attr"`
Size string `xml:"size,attr"`
Date string `xml:"date,attr"`
UpdatedDate string `xml:"updatedDate,attr"`
URL string `xml:"url,attr"`
Name string `xml:"name"`
ID string `xml:"id"`
Description string `xml:"description"`
Version string `xml:"version"`
Vendor string `xml:"vendor"`
Rating string `xml:"rating"`
ChangeNotes ChangeNotes `xml:"change-notes"`
IdeaVersion IdeaVersion `xml:"idea-version"`
}
)
type jbDownload struct {
fs afero.Fs
dist string
build string
}
func (d *jbDownload) Download(ctx context.Context, xid string) (*IdeaPlugin, error) {
location, _, err := d.meta(ctx, xid, d.build)
if err != nil {
return nil, err
}
parse, err := url.Parse(location)
if err != nil {
return nil, err
}
updateId := parse.Query().Get("updateId")
pluginId := parse.Query().Get("pluginId")
detail, _, err := d.detail(ctx, pluginId, updateId)
if err != nil {
return nil, err
}
if err = d.downloadAll(ctx, parse.Path); err != nil {
return nil, err
}
detail.URL = fmt.Sprintf("http://tools.sip.io/sis/jetbrains/plugins%s", parse.Path)
return detail, nil
}
func (d *jbDownload) downloadAll(ctx context.Context, filename string) error {
output := path.Join(d.dist, filename)
err := d.fs.MkdirAll(filepath.Dir(output), os.ModeDir)
if err != nil {
return err
}
collection := []string{""}
if strings.HasSuffix(filename, ".zip") {
collection = append(collection, ".blockmap.zip", ".hash.json")
}
for _, suffix := range collection {
fd, err := d.fs.Create(output + suffix)
if err != nil {
return err
}
resp, err := d.fetchFile(ctx, filename+suffix, fd)
_ = fd.Close()
if err != nil {
if resp.StatusCode == http.StatusForbidden {
log.Println("download 403 ", filename)
_ = d.fs.RemoveAll(output + suffix)
continue
}
return err
}
}
return nil
}
func (d *jbDownload) addOptions(s string, opts interface{}) (string, error) {
v := reflect.ValueOf(opts)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}
u, err := url.Parse(s)
if err != nil {
return s, err
}
qs, err := query.Values(opts)
if err != nil {
return s, err
}
u.RawQuery = qs.Encode()
return u.String(), nil
}
func (d *jbDownload) buildRequest(ctx context.Context, method, path string, q any) (*http.Request, error) {
u, err := d.addOptions("https://plugins.jetbrains.com"+path, q)
if err != nil {
return nil, err
}
return http.NewRequestWithContext(ctx, method, u, http.NoBody)
}
func (d *jbDownload) meta(ctx context.Context, id, build string) (string, *http.Response, error) {
opts := struct {
ID string `url:"id"`
Build string `url:"build"`
Action string `url:"action"`
}{id, build, "download"}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := d.buildRequest(ctx, http.MethodHead, "/pluginManager", opts)
if err != nil {
return "", nil, err
}
resp, err := client.Do(req)
if err != nil {
return "", resp, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusMovedPermanently {
return "", resp, fmt.Errorf("get id error by %s", resp.Request.URL.String())
}
return resp.Header.Get("Location"), resp, nil
}
func (d *jbDownload) detail(ctx context.Context, pluginId, updateId string) (*IdeaPlugin, *http.Response, error) {
opts := struct {
PluginID string `url:"pluginId"`
}{pluginId}
req, err := d.buildRequest(ctx, http.MethodGet, "/plugins/list", opts)
if err != nil {
return nil, nil, err
}
var p PluginRepository
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, resp, err
}
defer resp.Body.Close()
err = xml.NewDecoder(resp.Body).Decode(&p)
if err != nil {
return nil, resp, err
}
return &p.Category.IdeaPlugin[0], resp, nil
}
func (d *jbDownload) fetchFile(ctx context.Context, path string, w io.Writer) (*http.Response, error) {
req, err := d.buildRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
client := &http.Client{}
log.Printf("fetchFile %s\n", req.URL)
resp, err := client.Do(req)
if err != nil {
return resp, err
}
defer resp.Body.Close()
_, _ = io.Copy(w, resp.Body)
return resp, nil
}
func NewJbDownload(fs afero.Fs, dist, build string) Downloader {
_ = fs.RemoveAll(dist)
return &jbDownload{fs: fs, dist: dist, build: build}
}
type repoExport struct {
fs afero.Fs
}
func (e *repoExport) Export(filename string, data any) error {
fd, err := e.fs.Create(filename)
if err != nil {
return err
}
encoder := xml.NewEncoder(fd)
encoder.Indent("", " ")
return encoder.Encode(data)
}
func NewRepoExport(fs afero.Fs) Exporter {
return &repoExport{fs: fs}
}
func main() {
fs := afero.NewOsFs()
xmlIds, err := NewTxtLoader(fs, "plugins_url.txt").Load()
if err != nil {
panic(err)
}
downloader := NewJbDownload(fs, "dist/plugins", "GO-241.14494.238")
type plugins struct {
Plugin []*IdeaPlugin `xml:"plugin"`
}
allPlugins := &plugins{}
for _, id := range xmlIds {
p, err := downloader.Download(context.Background(), id)
if err != nil {
log.Println("download eror", err)
continue
}
allPlugins.Plugin = append(allPlugins.Plugin, p)
}
err = NewRepoExport(fs).Export("dist/plugins/updatePlugins.xml", allPlugins)
if err != nil {
panic(err)
}
}
plugins_url.txt 文件格式
https://plugins.jetbrains.com/plugin/index?xmlId=com.chrisrm.idea.MaterialThemeUI
https://plugins.jetbrains.com/plugin/index?xmlId=izhangzhihao.rainbow.brackets
https://plugins.jetbrains.com/plugin/index?xmlId=com.intellij.tasks
https://plugins.jetbrains.com/plugin/index?xmlId=com.intellij.tasks.timeTracking
https://plugins.jetbrains.com/plugin/index?xmlId=org.toml.lang
最后
运行后在 dist/plugins 复制到内网即可 嘿嘿