forked from deanishe/go-safari
/
tabs.go
171 lines (148 loc) · 3.86 KB
/
tabs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Copyright (c) 2018 Dean Jackson <deanishe@deanishe.net>
// MIT Licence applies http://opensource.org/licenses/MIT
// Package cloud provides access to Safari's iCloud Tabs.
package cloud
import (
"bytes"
"compress/zlib"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/jpshackelford/go-safari/filefinder"
// sqlite3 registers itself with sql
_ "github.com/mattn/go-sqlite3"
)
var (
// DefaultTabsPath is the path to the default CloudTabs database.
DefaultTabsPath = filefinder.New([]string{
filepath.Join(os.Getenv("HOME"), "Library/Safari/CloudTabs.db"),
filepath.Join(os.Getenv("HOME"), "Library/Containers/com.apple.Safari/Data/Library/Safari/CloudTabs.db"),
}).FirstExists()
hostname string
tabs *CloudTabs
)
func init() {
var (
data []byte
err error
)
tabs, err = New(DefaultTabsPath)
if err != nil {
panic(err)
}
data, err = exec.Command("/usr/sbin/scutil", "--get", "ComputerName").Output()
if err != nil {
panic(err)
}
hostname = strings.TrimSpace(string(data))
}
// CloudTabs is a collection of Tabs.
type CloudTabs struct {
DB *sql.DB
}
// New creates a new Tabs from a Safari CloudTabs.db database.
func New(filename string) (*CloudTabs, error) {
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro&cache=shared&_timeout=9999999&_journal=WAL", filename))
if err != nil {
return nil, fmt.Errorf("couldn't open database %s: %s", filename, err)
}
return &CloudTabs{db}, nil
}
// Tabs returns all Cloud Tabs. Tabs for the current device are ignored.
func Tabs() ([]*Tab, error) { return tabs.Tabs() }
// Tabs returns all Cloud Tabs. Tabs for the current device are ignored.
func (c *CloudTabs) Tabs() ([]*Tab, error) {
var (
q = `
SELECT t.title, t.url, t.position, d.device_name
FROM cloud_tabs t
LEFT JOIN cloud_tab_devices d
ON t.device_uuid = d.device_uuid
WHERE d.device_name != ?
`
title, url, device string
position []byte
tab *Tab
tabs []*Tab
)
rows, err := c.DB.Query(q, hostname)
if err != nil {
return nil, fmt.Errorf("error running query:%s error: %s", q, err)
}
defer rows.Close()
for rows.Next() {
rows.Scan(&title, &url, &position, &device)
tab = &Tab{Title: title, URL: url, Device: device}
sData, err := parsePosition(position)
if err != nil {
return nil, err
}
if len(sData) > 0 {
tab.SortIndex = sData[0].SortValue
}
tabs = append(tabs, tab)
}
sort.Sort(ByDeviceIndex(tabs))
return tabs, nil
}
// ByDeviceIndex sorts Tabs by device name and sort index.
type ByDeviceIndex []*Tab
// Implement sort.Interface
func (t ByDeviceIndex) Len() int { return len(t) }
func (t ByDeviceIndex) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t ByDeviceIndex) Less(i, j int) bool {
if t[i].Device < t[j].Device {
return true
}
if t[j].Device < t[i].Device {
return false
}
if t[i].SortIndex < t[j].SortIndex {
return true
}
return false
}
// Tab is a cloud tab.
type Tab struct {
Title string // Tab title
URL string // URL
Device string // Computer/phone/tablet name
SortIndex int // sortValue from position blob
}
// JSON objects contained in position blob
type sortData struct {
ChangeID int `json:"changeID"`
SortValue int `json:"sortValue"`
Device string `json"deviceIdentifier"`
}
// Parse the `position` blob. It's zlib-compressed JSON.
//
// {
// "sortValues": [
// {"changeID": int, "sortValue": int, "deviceIdentifier": string}
// ]
// }
func parsePosition(blob []byte) ([]sortData, error) {
b := bytes.NewBuffer(blob)
r, err := zlib.NewReader(b)
if err != nil {
return nil, err
}
defer r.Close()
data, err := ioutil.ReadAll(r)
vals := struct {
Vals []sortData `json:"sortValues"`
}{
Vals: []sortData{},
}
if err := json.Unmarshal(data, &vals); err != nil {
return nil, err
}
return vals.Vals, nil
}