Skip to content

Commit c7e8197

Browse files
committed
hs remote search support
Adds support to hs search using a remote HashUp API server.
1 parent fb20607 commit c7e8197

File tree

4 files changed

+131
-41
lines changed

4 files changed

+131
-41
lines changed

TODO

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
* Cache file stats when updating the database
44
* Remote search: use a remote node for file search, which means the remote node should have a copy of the file database
55
* Config migrations: figure out how to migrate to newer nats and hashup config versions
6+
* Add config section for API
7+
* Advanced query support when using API (extension, host, etc)

cmd/hs/search.go

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"strings"
77

88
_ "github.com/mattn/go-sqlite3"
9+
"github.com/rubiojr/hashup/cmd/hs/types"
10+
"github.com/rubiojr/hashup/internal/api"
11+
hsdb "github.com/rubiojr/hashup/internal/db"
912
"github.com/urfave/cli/v2"
1013
)
1114

@@ -15,10 +18,10 @@ func commandSearch() *cli.Command {
1518
Aliases: []string{"s"},
1619
Usage: "Search for files by filename",
1720
Flags: []cli.Flag{
18-
&cli.StringFlag{
21+
&cli.IntFlag{
1922
Name: "limit",
2023
Usage: "Number of results to return",
21-
Value: "100",
24+
Value: 100,
2225
Required: false,
2326
},
2427
&cli.StringFlag{
@@ -44,20 +47,30 @@ func commandSearch() *cli.Command {
4447
Value: "",
4548
Required: false,
4649
},
50+
&cli.StringFlag{
51+
Name: "server-url",
52+
Usage: "HashUp API server URL",
53+
Required: false,
54+
},
4755
},
4856
Action: func(c *cli.Context) error {
4957
hostFilter := c.String("host")
5058
extFilter := c.String("extension")
59+
serverURL := c.String("server-url")
5160

52-
if c.String("tag") != "" {
53-
return searchByTag(c)
54-
}
55-
61+
filename := c.Args().Get(0)
5662
if c.NArg() == 0 {
5763
return fmt.Errorf("filename argument is required")
5864
}
5965

60-
filename := c.Args().Get(0)
66+
if serverURL != "" {
67+
return searchServer(serverURL, filename)
68+
}
69+
70+
if c.String("tag") != "" {
71+
return searchByTag(c)
72+
}
73+
6174
if hostFilter != "" {
6275
return searchByHost(c, filename)
6376
}
@@ -95,27 +108,19 @@ func searchByTag(c *cli.Context) error {
95108

96109
func searchByExt(c *cli.Context, filename string) error {
97110
ext := c.String("ext")
98-
limit := c.String("limit")
111+
limit := c.Int("limit")
99112
db, err := dbConn(c.String("db"))
100113
if err != nil {
101114
return fmt.Errorf("failed to get database connection: %v", err)
102115
}
103116
defer db.Close()
104117

105-
query := `
106-
SELECT file_path, file_size, modified_date, host, extension, file_hash
107-
FROM file_info
108-
WHERE (file_path LIKE ? OR file_hash LIKE ?) AND extension = ?
109-
LIMIT ?
110-
`
111-
112-
rows, err := db.Query(query, "%"+filename+"%", "%"+filename+"%", ext, limit)
113-
if err != nil {
114-
return fmt.Errorf("failed to query database: %v", err)
118+
r, err := hsdb.Search(db, filename, []string{ext}, limit)
119+
for _, result := range r {
120+
printFileResult(result)
115121
}
116-
defer rows.Close()
117122

118-
return printRows(rows)
123+
return nil
119124
}
120125

121126
func searchByHost(c *cli.Context, filename string) error {
@@ -150,20 +155,12 @@ func searchFiles(c *cli.Context, filename string) error {
150155
}
151156
defer db.Close()
152157

153-
query := `
154-
SELECT file_path, file_size, modified_date, host, extension, file_hash
155-
FROM file_info
156-
WHERE file_path LIKE ? OR file_hash LIKE ?
157-
LIMIT 100
158-
`
159-
160-
rows, err := db.Query(query, "%"+filename+"%", "%"+filename+"%")
161-
if err != nil {
162-
return fmt.Errorf("failed to query database: %v", err)
158+
r, err := hsdb.Search(db, filename, []string{}, 100)
159+
for _, result := range r {
160+
printFileResult(result)
163161
}
164-
defer rows.Close()
165162

166-
return printRows(rows)
163+
return nil
167164
}
168165

169166
func printRows(rows *sql.Rows) error {
@@ -193,3 +190,27 @@ func printRows(rows *sql.Rows) error {
193190

194191
return nil
195192
}
193+
194+
func searchServer(serverURL string, filename string) error {
195+
client := api.NewClient(serverURL)
196+
r, err := client.Search(filename)
197+
if err != nil {
198+
return fmt.Errorf("failed to search server: %v", err)
199+
}
200+
201+
for _, result := range r {
202+
printFileResult(result)
203+
}
204+
205+
return nil
206+
}
207+
208+
func printFileResult(result *types.FileResult) {
209+
fmt.Printf("File Path: %s\n", result.FilePath)
210+
fmt.Printf("File Size: %d bytes\n", result.FileSize)
211+
fmt.Printf("Modified Date: %s\n", result.ModifiedDate)
212+
fmt.Printf("Host: %s\n", result.Host)
213+
fmt.Printf("Extension: %s\n", result.Extension)
214+
fmt.Printf("Hash: %s\n", result.FileHash)
215+
fmt.Println(strings.Repeat("-", 40))
216+
}

internal/api/api.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ package api
22

33
import (
44
"database/sql"
5+
"encoding/json"
56
"errors"
67
"fmt"
8+
"io"
9+
"net"
710
"net/http"
11+
"net/url"
812
"strconv"
913
"time"
1014

1115
"github.com/go-chi/chi/v5"
1216
"github.com/go-chi/chi/v5/middleware"
1317
"github.com/go-chi/render"
1418
_ "github.com/mattn/go-sqlite3"
19+
"github.com/rubiojr/hashup/cmd/hs/types"
1520
"github.com/rubiojr/hashup/internal/config"
1621
hsdb "github.com/rubiojr/hashup/internal/db"
1722
)
@@ -36,6 +41,70 @@ func Serve(cfgPath string, addr string) error {
3641
return nil
3742
}
3843

44+
type Client struct {
45+
client *http.Client
46+
serverURL string
47+
}
48+
49+
func NewClient(serverURL string) *Client {
50+
client := &http.Client{
51+
Timeout: 10 * time.Second,
52+
Transport: &http.Transport{
53+
IdleConnTimeout: 90 * time.Second,
54+
Dial: (&net.Dialer{
55+
Timeout: 5 * time.Second,
56+
}).Dial,
57+
TLSHandshakeTimeout: 5 * time.Second,
58+
},
59+
}
60+
return &Client{client: client, serverURL: serverURL}
61+
}
62+
63+
func (c *Client) Search(query string) ([]*types.FileResult, error) {
64+
// Build the URL with query parameters
65+
urlStr := fmt.Sprintf("%s/search?q=%s", c.serverURL, url.QueryEscape(query))
66+
67+
// Create request
68+
req, err := http.NewRequest("GET", urlStr, nil)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to create request: %w", err)
71+
}
72+
73+
// Set headers
74+
req.Header.Set("Accept", "application/json")
75+
76+
// Execute request
77+
resp, err := c.client.Do(req)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to execute request: %w", err)
80+
}
81+
defer resp.Body.Close()
82+
83+
// Check response status
84+
if resp.StatusCode != http.StatusOK {
85+
// Try to read error message
86+
body, _ := io.ReadAll(resp.Body)
87+
88+
// Parse error response if possible
89+
var errorResp struct {
90+
Error string `json:"error"`
91+
}
92+
if err := json.Unmarshal(body, &errorResp); err == nil && errorResp.Error != "" {
93+
return nil, fmt.Errorf("server returned error: %s (status: %d)", errorResp.Error, resp.StatusCode)
94+
}
95+
96+
return nil, fmt.Errorf("server returned non-OK status: %d", resp.StatusCode)
97+
}
98+
99+
// Parse response body
100+
var results []*types.FileResult
101+
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
102+
return nil, fmt.Errorf("failed to decode response: %w", err)
103+
}
104+
105+
return results, nil
106+
}
107+
39108
func statusJSON(code int, err error, w http.ResponseWriter, r *http.Request) {
40109
if err != nil {
41110
w.WriteHeader(code)

internal/db/db.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,26 @@ func Search(db *sql.DB, query string, extensions []string, limit int) ([]*types.
5959
WHERE (file_path LIKE ? OR file_hash LIKE ?)
6060
`
6161

62+
var args []any
63+
args = append(args, "%"+query+"%", "%"+query+"%")
64+
6265
if len(extensions) > 0 {
6366
placeholders := make([]string, len(extensions))
6467
for i := range extensions {
6568
placeholders[i] = "?"
6669
}
6770
sqlQuery += fmt.Sprintf(" AND extension IN (%s)", strings.Join(placeholders, ","))
71+
for _, ext := range extensions {
72+
args = append(args, strings.TrimSpace(ext))
73+
}
6874
}
6975

7076
sqlQuery += `
7177
ORDER BY modified_date DESC
7278
`
7379

74-
var args []any
75-
args = append(args, "%"+query+"%", "%"+query+"%")
76-
77-
if len(extensions) > 0 {
78-
for _, ext := range extensions {
79-
args = append(args, strings.TrimSpace(ext))
80-
}
81-
}
82-
8380
sqlQuery += fmt.Sprintf("LIMIT %d", limit)
81+
fmt.Println(sqlQuery)
8482

8583
rows, err := db.Query(sqlQuery, args...)
8684
if err != nil {

0 commit comments

Comments
 (0)