plugin_repo.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package repository
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/json"
  6. "fmt"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. )
  10. type pluginRepository struct {
  11. db *sql.DB
  12. }
  13. func NewPluginRepository(db *sql.DB) service.PluginRepository {
  14. return &pluginRepository{db: db}
  15. }
  16. func (r *pluginRepository) List(ctx context.Context) ([]*service.PluginInstallation, error) {
  17. rows, err := r.db.QueryContext(ctx, pluginSelectSQL+` ORDER BY installed_at DESC, id DESC`)
  18. if err != nil {
  19. return nil, err
  20. }
  21. defer func() { _ = rows.Close() }()
  22. plugins := make([]*service.PluginInstallation, 0)
  23. for rows.Next() {
  24. plugin, scanErr := scanPlugin(rows)
  25. if scanErr != nil {
  26. return nil, scanErr
  27. }
  28. plugins = append(plugins, plugin)
  29. }
  30. if err := rows.Err(); err != nil {
  31. return nil, err
  32. }
  33. for _, plugin := range plugins {
  34. bindings, bindingErr := r.listBindings(ctx, plugin.ID)
  35. if bindingErr != nil {
  36. return nil, bindingErr
  37. }
  38. plugin.Bindings = bindings
  39. }
  40. return plugins, nil
  41. }
  42. func (r *pluginRepository) GetByID(ctx context.Context, id int64) (*service.PluginInstallation, error) {
  43. plugin, err := scanPlugin(r.db.QueryRowContext(ctx, pluginSelectSQL+` WHERE id = $1`, id))
  44. if err != nil {
  45. return nil, err
  46. }
  47. plugin.Bindings, err = r.listBindings(ctx, plugin.ID)
  48. return plugin, err
  49. }
  50. func (r *pluginRepository) GetByKey(ctx context.Context, key string) (*service.PluginInstallation, error) {
  51. plugin, err := scanPlugin(r.db.QueryRowContext(ctx, pluginSelectSQL+` WHERE plugin_key = $1`, key))
  52. if err != nil {
  53. return nil, err
  54. }
  55. plugin.Bindings, err = r.listBindings(ctx, plugin.ID)
  56. return plugin, err
  57. }
  58. func (r *pluginRepository) Install(ctx context.Context, plugin *service.PluginInstallation, bindings []service.PluginBinding) (*service.PluginInstallation, error) {
  59. manifestJSON, err := json.Marshal(plugin.Manifest)
  60. if err != nil {
  61. return nil, fmt.Errorf("序列化插件清单: %w", err)
  62. }
  63. tx, err := r.db.BeginTx(ctx, nil)
  64. if err != nil {
  65. return nil, err
  66. }
  67. defer func() { _ = tx.Rollback() }()
  68. row := tx.QueryRowContext(ctx, `
  69. INSERT INTO sub2api_plugin_installations (
  70. plugin_key, name, version, description, author, manifest, artifact_data,
  71. artifact_path, install_path, binary_path, binary_sha256,
  72. signature_status, state, last_error, installed_by, installed_at, updated_at
  73. ) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10, $11, $12, $13, '', $14, NOW(), NOW())
  74. ON CONFLICT (plugin_key) DO UPDATE SET
  75. name = EXCLUDED.name,
  76. version = EXCLUDED.version,
  77. description = EXCLUDED.description,
  78. author = EXCLUDED.author,
  79. manifest = EXCLUDED.manifest,
  80. artifact_data = EXCLUDED.artifact_data,
  81. artifact_path = EXCLUDED.artifact_path,
  82. install_path = EXCLUDED.install_path,
  83. binary_path = EXCLUDED.binary_path,
  84. binary_sha256 = EXCLUDED.binary_sha256,
  85. signature_status = EXCLUDED.signature_status,
  86. state = EXCLUDED.state,
  87. last_error = '',
  88. installed_by = EXCLUDED.installed_by,
  89. installed_at = NOW(),
  90. enabled_at = NULL,
  91. updated_at = NOW()
  92. WHERE sub2api_plugin_installations.state IN ('disabled', 'error', 'incompatible')
  93. AND NOT EXISTS (
  94. SELECT 1 FROM sub2api_plugin_bindings b
  95. WHERE b.plugin_id = sub2api_plugin_installations.id AND b.enabled = TRUE
  96. )
  97. RETURNING id
  98. `, plugin.PluginKey, plugin.Name, plugin.Version, plugin.Description, plugin.Author, manifestJSON, plugin.ArtifactData,
  99. plugin.ArtifactPath, plugin.InstallPath, plugin.BinaryPath, plugin.BinarySHA256,
  100. plugin.SignatureStatus, plugin.State, plugin.InstalledBy)
  101. var id int64
  102. if err := row.Scan(&id); err != nil {
  103. if err == sql.ErrNoRows {
  104. return nil, service.ErrPluginStateChanged
  105. }
  106. return nil, err
  107. }
  108. if err := replacePluginBindings(ctx, tx, id, bindings); err != nil {
  109. return nil, err
  110. }
  111. if err := tx.Commit(); err != nil {
  112. return nil, err
  113. }
  114. return r.GetByID(ctx, id)
  115. }
  116. func (r *pluginRepository) GetArtifact(ctx context.Context, id int64) ([]byte, error) {
  117. var artifact []byte
  118. err := r.db.QueryRowContext(ctx, `SELECT artifact_data FROM sub2api_plugin_installations WHERE id = $1`, id).Scan(&artifact)
  119. return artifact, err
  120. }
  121. func (r *pluginRepository) Delete(ctx context.Context, id int64, expectedBinarySHA256 string) error {
  122. result, err := r.db.ExecContext(ctx, `
  123. DELETE FROM sub2api_plugin_installations p
  124. WHERE p.id = $1 AND p.binary_sha256 = $2 AND p.state NOT IN ('starting', 'enabled')
  125. AND NOT EXISTS (SELECT 1 FROM sub2api_plugin_bindings b WHERE b.plugin_id = p.id AND b.enabled = TRUE)
  126. `, id, expectedBinarySHA256)
  127. if err != nil {
  128. return err
  129. }
  130. rows, err := result.RowsAffected()
  131. if err != nil {
  132. return err
  133. }
  134. if rows != 1 {
  135. return service.ErrPluginStateChanged
  136. }
  137. return nil
  138. }
  139. func (r *pluginRepository) BeginEnable(ctx context.Context, id int64, binarySHA256, expectedState string) error {
  140. result, err := r.db.ExecContext(ctx, `
  141. UPDATE sub2api_plugin_installations
  142. SET state = 'starting', last_error = '', updated_at = NOW()
  143. WHERE id = $1 AND binary_sha256 = $2 AND state = $3 AND state <> 'starting'
  144. `, id, binarySHA256, expectedState)
  145. if err != nil {
  146. return err
  147. }
  148. rows, err := result.RowsAffected()
  149. if err != nil {
  150. return err
  151. }
  152. if rows != 1 {
  153. return service.ErrPluginStateChanged
  154. }
  155. return nil
  156. }
  157. func (r *pluginRepository) MarkRuntimeHealthy(ctx context.Context, id int64, binarySHA256, configEncrypted string) error {
  158. result, err := r.db.ExecContext(ctx, `
  159. UPDATE sub2api_plugin_installations p
  160. SET state = 'enabled', last_error = '', enabled_at = COALESCE(enabled_at, NOW()), updated_at = NOW()
  161. WHERE p.id = $1 AND p.binary_sha256 = $2 AND p.config_encrypted = $3
  162. AND EXISTS (SELECT 1 FROM sub2api_plugin_bindings b WHERE b.plugin_id = p.id AND b.enabled = TRUE)
  163. `, id, binarySHA256, configEncrypted)
  164. if err != nil {
  165. return err
  166. }
  167. rows, err := result.RowsAffected()
  168. if err != nil {
  169. return err
  170. }
  171. if rows != 1 {
  172. return service.ErrPluginStateChanged
  173. }
  174. return nil
  175. }
  176. func (r *pluginRepository) UpdateState(ctx context.Context, id int64, state, lastError string, enabledAt *time.Time, expectedBinarySHA256, expectedState string) error {
  177. result, err := r.db.ExecContext(ctx, `
  178. UPDATE sub2api_plugin_installations
  179. SET state = $2, last_error = $3, enabled_at = $4, updated_at = NOW()
  180. WHERE id = $1 AND binary_sha256 = $5 AND state = $6
  181. `, id, state, lastError, enabledAt, expectedBinarySHA256, expectedState)
  182. if err != nil {
  183. return err
  184. }
  185. rows, err := result.RowsAffected()
  186. if err != nil {
  187. return err
  188. }
  189. if rows != 1 {
  190. return service.ErrPluginStateChanged
  191. }
  192. return nil
  193. }
  194. func (r *pluginRepository) UpdateConfig(ctx context.Context, id int64, encrypted, expectedBinarySHA256 string) error {
  195. result, err := r.db.ExecContext(ctx, `
  196. UPDATE sub2api_plugin_installations
  197. SET config_encrypted = $2, updated_at = NOW()
  198. WHERE id = $1 AND binary_sha256 = $3
  199. `, id, encrypted, expectedBinarySHA256)
  200. if err != nil {
  201. return err
  202. }
  203. rows, err := result.RowsAffected()
  204. if err != nil {
  205. return err
  206. }
  207. if rows != 1 {
  208. return service.ErrPluginStateChanged
  209. }
  210. return nil
  211. }
  212. func (r *pluginRepository) UpdateBindingsAndState(
  213. ctx context.Context,
  214. pluginID int64,
  215. bindings []service.PluginBinding,
  216. state string,
  217. lastError string,
  218. enabledAt *time.Time,
  219. expectedState string,
  220. expectedBinarySHA256 string,
  221. ) error {
  222. tx, err := r.db.BeginTx(ctx, nil)
  223. if err != nil {
  224. return err
  225. }
  226. defer func() { _ = tx.Rollback() }()
  227. result, err := tx.ExecContext(ctx, `
  228. UPDATE sub2api_plugin_installations
  229. SET state = $2, last_error = $3, enabled_at = $4, updated_at = NOW()
  230. WHERE id = $1 AND ($5 = '' OR state = $5) AND binary_sha256 = $6
  231. `, pluginID, state, lastError, enabledAt, expectedState, expectedBinarySHA256)
  232. if err != nil {
  233. return err
  234. }
  235. rows, err := result.RowsAffected()
  236. if err != nil {
  237. return err
  238. }
  239. if rows != 1 {
  240. return service.ErrPluginStateChanged
  241. }
  242. if err := replacePluginBindings(ctx, tx, pluginID, bindings); err != nil {
  243. return err
  244. }
  245. return tx.Commit()
  246. }
  247. type pluginBindingExecutor interface {
  248. ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
  249. }
  250. func replacePluginBindings(ctx context.Context, executor pluginBindingExecutor, pluginID int64, bindings []service.PluginBinding) error {
  251. if _, err := executor.ExecContext(ctx, `DELETE FROM sub2api_plugin_bindings WHERE plugin_id = $1`, pluginID); err != nil {
  252. return err
  253. }
  254. for _, binding := range bindings {
  255. if _, err := executor.ExecContext(ctx, `
  256. INSERT INTO sub2api_plugin_bindings (
  257. plugin_id, capability, platform, account_type, enabled, rollout_percent, created_at, updated_at
  258. ) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
  259. `, pluginID, binding.Capability, binding.Platform, binding.AccountType, binding.Enabled, binding.RolloutPercent); err != nil {
  260. return err
  261. }
  262. }
  263. return nil
  264. }
  265. const pluginSelectSQL = `
  266. SELECT id, plugin_key, name, version, description, author, manifest,
  267. artifact_path, install_path, binary_path, binary_sha256,
  268. signature_status, state, config_encrypted, last_error,
  269. installed_by, installed_at, enabled_at, updated_at
  270. FROM sub2api_plugin_installations`
  271. type pluginScanner interface {
  272. Scan(dest ...any) error
  273. }
  274. func scanPlugin(scanner pluginScanner) (*service.PluginInstallation, error) {
  275. plugin := &service.PluginInstallation{}
  276. var manifestJSON []byte
  277. if err := scanner.Scan(
  278. &plugin.ID, &plugin.PluginKey, &plugin.Name, &plugin.Version, &plugin.Description,
  279. &plugin.Author, &manifestJSON, &plugin.ArtifactPath, &plugin.InstallPath,
  280. &plugin.BinaryPath, &plugin.BinarySHA256, &plugin.SignatureStatus, &plugin.State,
  281. &plugin.ConfigEncrypted, &plugin.LastError, &plugin.InstalledBy, &plugin.InstalledAt,
  282. &plugin.EnabledAt, &plugin.UpdatedAt,
  283. ); err != nil {
  284. return nil, err
  285. }
  286. if err := json.Unmarshal(manifestJSON, &plugin.Manifest); err != nil {
  287. return nil, fmt.Errorf("解析插件清单: %w", err)
  288. }
  289. return plugin, nil
  290. }
  291. func (r *pluginRepository) listBindings(ctx context.Context, pluginID int64) ([]service.PluginBinding, error) {
  292. rows, err := r.db.QueryContext(ctx, `
  293. SELECT id, plugin_id, capability, platform, account_type, enabled,
  294. rollout_percent, created_at, updated_at
  295. FROM sub2api_plugin_bindings WHERE plugin_id = $1 ORDER BY id
  296. `, pluginID)
  297. if err != nil {
  298. return nil, err
  299. }
  300. defer func() { _ = rows.Close() }()
  301. bindings := make([]service.PluginBinding, 0)
  302. for rows.Next() {
  303. var binding service.PluginBinding
  304. if err := rows.Scan(&binding.ID, &binding.PluginID, &binding.Capability, &binding.Platform,
  305. &binding.AccountType, &binding.Enabled, &binding.RolloutPercent,
  306. &binding.CreatedAt, &binding.UpdatedAt); err != nil {
  307. return nil, err
  308. }
  309. bindings = append(bindings, binding)
  310. }
  311. return bindings, rows.Err()
  312. }
  313. var _ service.PluginRepository = (*pluginRepository)(nil)