Skip to content

Commit b8060fd

Browse files
authored
Feature: Add snap origin (#428)
1 parent 97a81d0 commit b8060fd

9 files changed

Lines changed: 626 additions & 297 deletions

File tree

README.md

Lines changed: 285 additions & 296 deletions
Large diffs are not rendered by default.

internal/consts/origins.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,17 @@ const (
99
OriginPacman = "pacman"
1010
OriginPipx = "pipx"
1111
OriginRpm = "rpm"
12+
OriginSnap = "snap"
1213
)
14+
15+
var ValidOrigins = []string{
16+
OriginBrew,
17+
OriginDeb,
18+
OriginFlatpak,
19+
OriginNpm,
20+
OriginOpkg,
21+
OriginPacman,
22+
OriginPipx,
23+
OriginRpm,
24+
OriginSnap,
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package snap
2+
3+
const (
4+
snapRoot = "/snap"
5+
6+
networkUnix = "unix"
7+
snapdSocket = "/run/snapd.socket"
8+
snapLocalHost = "http://localhost/v2/snaps"
9+
connectionLocalHost = "http://localhost/v2/connections"
10+
11+
typeOs = "os"
12+
typeSnapd = "snapd"
13+
14+
binDir = "/var/lib/snapd/snap/bin"
15+
snapsDir = "/var/lib/snapd/snaps"
16+
dotSnap = ".snap"
17+
18+
interfaceContent = "content"
19+
20+
timestampFormat = "2006-01-02T15:04:05.999999999-07:00"
21+
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package snap
2+
3+
import (
4+
"os"
5+
"qp/internal/consts"
6+
"qp/internal/origins/shared"
7+
"qp/internal/pkgdata"
8+
"qp/internal/storage"
9+
)
10+
11+
type SnapDriver struct{}
12+
13+
func (d *SnapDriver) Name() string {
14+
return consts.OriginSnap
15+
}
16+
17+
func (d *SnapDriver) Detect() bool {
18+
entries, err := os.ReadDir(snapRoot)
19+
if err != nil {
20+
return false
21+
}
22+
23+
hasSnaps := false
24+
for _, entry := range entries {
25+
if entry.IsDir() {
26+
hasSnaps = true
27+
break
28+
}
29+
}
30+
31+
return hasSnaps
32+
}
33+
34+
func (d *SnapDriver) Load(cacheRoot string) ([]*pkgdata.PkgInfo, error) {
35+
return fetchPackages()
36+
}
37+
38+
func (d *SnapDriver) ResolveDeps(pkgs []*pkgdata.PkgInfo) ([]*pkgdata.PkgInfo, error) {
39+
return pkgdata.ResolveDependencyGraph(pkgs, nil)
40+
}
41+
42+
func (d *SnapDriver) LoadCache(path string) ([]*pkgdata.PkgInfo, error) {
43+
return storage.LoadProtoCache(path)
44+
}
45+
46+
func (d *SnapDriver) SaveCache(cacheRoot string, pkgs []*pkgdata.PkgInfo) error {
47+
return storage.SaveProtoCache(cacheRoot, pkgs)
48+
}
49+
50+
func (d *SnapDriver) IsCacheStale(cacheMtime int64) (bool, error) {
51+
return shared.BfsStale(snapsDir, cacheMtime, 1)
52+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package snap
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"qp/internal/pkgdata"
10+
"time"
11+
)
12+
13+
func fetchPackages() ([]*pkgdata.PkgInfo, error) {
14+
client := &http.Client{
15+
Transport: &http.Transport{
16+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
17+
return net.Dial(networkUnix, snapdSocket)
18+
},
19+
},
20+
Timeout: 5 * time.Second,
21+
}
22+
23+
snapResp, err := fetchSnaps(client)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
connsResp, err := fetchConnections(client)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
deps := parseConnections(connsResp)
34+
35+
return parseSnaps(snapResp, deps)
36+
}
37+
38+
func fetchSnaps(client *http.Client) (SnapdResponse, error) {
39+
resp, err := client.Get(snapLocalHost)
40+
if err != nil {
41+
return SnapdResponse{}, fmt.Errorf("failed to connect to snapd: %w", err)
42+
}
43+
defer resp.Body.Close()
44+
45+
if resp.StatusCode != http.StatusOK {
46+
return SnapdResponse{}, fmt.Errorf("snapd API error: %d", resp.StatusCode)
47+
}
48+
49+
var snapRep SnapdResponse
50+
if err := json.NewDecoder(resp.Body).Decode(&snapRep); err != nil {
51+
return SnapdResponse{}, fmt.Errorf("failed to parse snapd response: %w", err)
52+
}
53+
54+
return snapRep, nil
55+
}
56+
57+
func fetchConnections(client *http.Client) (ConnectionsResponse, error) {
58+
resp, err := client.Get("http://localhost/v2/connections")
59+
if err != nil {
60+
return ConnectionsResponse{}, err
61+
}
62+
defer resp.Body.Close()
63+
64+
var connsResp ConnectionsResponse
65+
if err := json.NewDecoder(resp.Body).Decode(&connsResp); err != nil {
66+
return ConnectionsResponse{}, err
67+
}
68+
69+
return connsResp, nil
70+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package snap
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"qp/internal/consts"
8+
"qp/internal/pkgdata"
9+
"time"
10+
)
11+
12+
type SnapdResponse struct {
13+
Type string `json:"type"`
14+
Status string `json:"status"`
15+
Result []SnapInfo `json:"result"`
16+
}
17+
18+
type SnapInfo struct {
19+
InstalledSize int64 `json:"installed-size"`
20+
Name string `json:"name"`
21+
Title string `json:"title"`
22+
Base string `json:"base"`
23+
Type string `json:"type"`
24+
Version string `json:"version"`
25+
Revision string `json:"revision"`
26+
InstallDate string `json:"install-date"`
27+
RefreshDate string `json:"refresh-date"`
28+
Status string `json:"status"`
29+
License string `json:"license"`
30+
Summary string `json:"summary"`
31+
Description string `json:"description"`
32+
Developer string `json:"developer"`
33+
Publisher Publisher `json:"publisher"`
34+
Links Links `json:"links"`
35+
}
36+
37+
type Publisher struct {
38+
ID string `json:"id"`
39+
Username string `json:"username"`
40+
DisplayName string `json:"display-name"`
41+
Validation string `json:"validation"`
42+
}
43+
44+
type Links struct {
45+
Website []string `json:"website"`
46+
Source []string `json:"source"`
47+
Issues []string `json:"issues"`
48+
}
49+
50+
type ConnectionsResponse struct {
51+
Type string `json:"type"`
52+
Status string `json:"status"`
53+
Result Result `json:"result"`
54+
}
55+
56+
type Result struct {
57+
Established []Connection `json:"established"`
58+
}
59+
60+
type Connection struct {
61+
Interface string `json:"interface"`
62+
Slot Slot `json:"slot"`
63+
Plug Plug `json:"plug"`
64+
}
65+
66+
type Slot struct {
67+
Snap string `json:"snap"`
68+
Slot string `json:"slot"`
69+
}
70+
71+
type Plug struct {
72+
Snap string `json:"snap"`
73+
Plug string `json:"plug"`
74+
}
75+
76+
func parseSnaps(snapResp SnapdResponse, deps map[string][]string) ([]*pkgdata.PkgInfo, error) {
77+
pkgs := make([]*pkgdata.PkgInfo, 0, len(snapResp.Result))
78+
79+
for _, snapInfo := range snapResp.Result {
80+
depends := make([]pkgdata.Relation, 0, len(deps[snapInfo.Name]))
81+
for _, dep := range deps[snapInfo.Name] {
82+
depends = append(depends, pkgdata.Relation{
83+
Name: dep,
84+
Depth: 1,
85+
})
86+
}
87+
88+
reason := consts.ReasonExplicit
89+
if snapInfo.Type != typeOs && snapInfo.Type != typeSnapd {
90+
binPath := filepath.Join(binDir, snapInfo.Name)
91+
_, err := os.Stat(binPath)
92+
if err != nil {
93+
reason = consts.ReasonDependency
94+
}
95+
}
96+
97+
if snapInfo.Base != "" {
98+
depends = append(depends, pkgdata.Relation{
99+
Name: snapInfo.Base,
100+
Depth: 1,
101+
})
102+
}
103+
104+
var updateTimestamp int64
105+
snapFile := fmt.Sprintf("%s_%s%s", snapInfo.Name, snapInfo.Revision, dotSnap)
106+
snapPath := filepath.Join(snapsDir, snapFile)
107+
fileInfo, err := os.Stat(snapPath)
108+
if err == nil {
109+
updateTimestamp = fileInfo.ModTime().Unix()
110+
}
111+
112+
pkg := &pkgdata.PkgInfo{
113+
UpdateTimestamp: updateTimestamp,
114+
InstallTimestamp: parseTimestamp(snapInfo.InstallDate),
115+
Size: snapInfo.InstalledSize,
116+
Name: snapInfo.Name,
117+
Title: snapInfo.Title,
118+
Reason: reason,
119+
Version: snapInfo.Version,
120+
Origin: consts.OriginSnap,
121+
License: snapInfo.License,
122+
Description: snapInfo.Summary,
123+
Author: snapInfo.Developer,
124+
Url: getUrl(snapInfo.Links),
125+
Packager: snapInfo.Publisher.DisplayName,
126+
PkgType: snapInfo.Type,
127+
Validation: snapInfo.Publisher.Validation,
128+
Depends: depends,
129+
}
130+
131+
pkgs = append(pkgs, pkg)
132+
}
133+
134+
return pkgs, nil
135+
}
136+
137+
func parseConnections(connsResp ConnectionsResponse) map[string][]string {
138+
deps := make(map[string][]string)
139+
for _, conn := range connsResp.Result.Established {
140+
if conn.Interface == interfaceContent {
141+
deps[conn.Plug.Snap] = append(deps[conn.Plug.Snap], conn.Slot.Snap)
142+
}
143+
}
144+
145+
return deps
146+
}
147+
148+
func getUrl(links Links) string {
149+
for _, link := range links.Website {
150+
return link
151+
}
152+
153+
for _, link := range links.Source {
154+
return link
155+
}
156+
157+
for _, link := range links.Issues {
158+
return link
159+
}
160+
161+
return ""
162+
}
163+
164+
func parseTimestamp(timeStr string) int64 {
165+
if timeStr == "" {
166+
return 0
167+
}
168+
169+
timestamp, err := time.Parse(timestampFormat, timeStr)
170+
if err != nil {
171+
timestamp, err = time.Parse(time.RFC3339Nano, timeStr)
172+
173+
if err == nil {
174+
return timestamp.Unix()
175+
}
176+
177+
return 0
178+
}
179+
180+
return timestamp.Unix()
181+
}

internal/origins/registry.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"qp/internal/origins/drivers/pacman"
1111
"qp/internal/origins/drivers/pipx"
1212
"qp/internal/origins/drivers/rpm"
13+
"qp/internal/origins/drivers/snap"
1314
)
1415

1516
var registeredDrivers = []driver.Driver{
@@ -21,6 +22,7 @@ var registeredDrivers = []driver.Driver{
2122
&pacman.PacmanDriver{},
2223
&pipx.PipxDriver{},
2324
&rpm.RpmDriver{},
25+
&snap.SnapDriver{},
2426
}
2527

2628
func AvailableDrivers() []driver.Driver {

internal/pkgdata/pkginfo.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type PkgInfo struct {
1212
Freeable int64
1313
Footprint int64
1414
Name string
15+
Author string
1516
Title string
1617
Reason string
1718
Version string

internal/pkgdata/relation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Relation struct {
2121
Version string
2222
ProviderName string
2323
Why string
24-
PkgType string
24+
PkgType string // pkgtype should only be declared when there is a split ecosystem, such as formulae/casks in Brew
2525
}
2626

2727
func (rel *Relation) Key() string {

0 commit comments

Comments
 (0)