fix: исправление 6 пунктов из ревью
Critical: - bridge: AutoGenPort=false по умолчанию, не генерируем secret если пустой → extension и bridge совпадают на port 9786 и empty secret - bridgeConfig: убрана авто-генерация secret, убран secret из BridgeInfo High: - extension/background.js + extension-firefox/background.js: все chrome.* listeners вынесены в global scope (не внутри onInstalled/onStartup) → MV3 service worker корректно перезапускается - UI: acceptBrowserEvent вызывает AcceptBrowserEvent, attachBrowserEvent вызывает AttachBrowserEventToNode (к текущему selectedNode), а не DismissBrowserEvent - watcher: при Create проверяется isUnderVault(absPath, vaultRoot) — если файл уже в vault, используется AddExternal вместо CopyIntoVault → нет дублирования файлов с timestamp-суффиксом Medium: - bridge.Event: добавлено поле DeviceID, handleEvents обогащает events из batch.DeviceID → device_id сохраняется в DB как chrome-*/firefox-*, а не evt_* - config: FileWatcher изменён на *bool — nil означает default true, false = явно выключено → старые config.json без поля file_watcher получают true
This commit is contained in:
parent
b676ac675a
commit
1cc0c407b1
|
|
@ -44,13 +44,10 @@ func (a *App) startBridge(appCfg *config.AppConfig) {
|
|||
return
|
||||
}
|
||||
|
||||
// Save the actual port and secret back to config if auto-generated.
|
||||
// Save the actual port back to config if auto-generated.
|
||||
if bc.AutoGenPort {
|
||||
bc.Port = port
|
||||
}
|
||||
if bc.Secret == "" {
|
||||
bc.Secret = srv.Secret()
|
||||
}
|
||||
a.saveBridgeConfig(appCfg, bc)
|
||||
|
||||
a.mu.Lock()
|
||||
|
|
@ -58,20 +55,14 @@ func (a *App) startBridge(appCfg *config.AppConfig) {
|
|||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// bridgeConfig extracts bridge config from app config, generating defaults if needed.
|
||||
// bridgeConfig extracts bridge config from app config.
|
||||
func (a *App) bridgeConfig(appCfg *config.AppConfig) *config.BridgeConfig {
|
||||
if appCfg != nil && appCfg.Vault.Bridge.Port != 0 {
|
||||
bc := &appCfg.Vault.Bridge
|
||||
// If secret is empty, generate one on first run
|
||||
if bc.Secret == "" {
|
||||
bc.Secret = bridge.GenerateSecret()
|
||||
}
|
||||
return bc
|
||||
return &appCfg.Vault.Bridge
|
||||
}
|
||||
return &config.BridgeConfig{
|
||||
Port: 9786,
|
||||
AutoGenPort: true,
|
||||
Secret: bridge.GenerateSecret(),
|
||||
AutoGenPort: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,20 +91,15 @@ func (a *App) BridgeInfo() map[string]interface{} {
|
|||
if a.bridge != nil {
|
||||
info["running"] = a.bridge.Running()
|
||||
info["port"] = a.bridge.Port()
|
||||
info["secret"] = a.bridge.Secret()
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// bridgeToBrowser converts a bridge.Event to a browser.Event.
|
||||
func bridgeToBrowser(ev bridge.Event) browser.Event {
|
||||
deviceID := ev.ID
|
||||
if idx := indexOf(ev.ID, "_"); idx > 0 {
|
||||
deviceID = ev.ID[:idx]
|
||||
}
|
||||
return browser.Event{
|
||||
ID: ev.ID,
|
||||
DeviceID: deviceID,
|
||||
DeviceID: ev.DeviceID,
|
||||
Type: ev.Type,
|
||||
URL: ev.URL,
|
||||
Title: ev.Title,
|
||||
|
|
@ -126,12 +112,3 @@ func bridgeToBrowser(ev bridge.Event) browser.Event {
|
|||
Note: ev.Note,
|
||||
}
|
||||
}
|
||||
|
||||
func indexOf(s string, sub string) int {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if string(s[i]) == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,8 +237,8 @@ func (a *App) initVault(vaultPath string) error {
|
|||
// Determine if real-time watching is enabled.
|
||||
// Priority: CLI --no-watcher > env VERSTAK_NO_WATCHER > config file > default (true)
|
||||
fileWatcherEnabled := true
|
||||
if appCfg != nil {
|
||||
fileWatcherEnabled = appCfg.Vault.FileWatcher
|
||||
if appCfg != nil && appCfg.Vault.FileWatcher != nil {
|
||||
fileWatcherEnabled = *appCfg.Vault.FileWatcher
|
||||
}
|
||||
// Env override
|
||||
if os.Getenv("VERSTAK_NO_WATCHER") == "1" {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func (a *App) ToggleFileWatcher(enable bool) error {
|
|||
if err != nil || cfg == nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
cfg.Vault.FileWatcher = enable
|
||||
cfg.Vault.FileWatcher = config.BoolPtr(enable)
|
||||
if err := config.SaveAppConfig(cfg); err != nil {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// Verstak Bridge for Firefox — Background Service Worker
|
||||
// Uses browser.* WebExtensions API with chrome.* polyfill layer.
|
||||
// Functionally identical to the Chrome version.
|
||||
//
|
||||
// IMPORTANT: All event listeners are registered at global scope,
|
||||
// NOT inside onInstalled/onStartup. MV3 service workers can be restarted at
|
||||
// any time; listeners inside lifecycle hooks are lost on restart.
|
||||
|
||||
// Firefox uses browser.* namespace; provide chrome.* aliases for shared code.
|
||||
if (typeof browser !== 'undefined' && typeof chrome === 'undefined') {
|
||||
|
|
@ -10,7 +13,7 @@ if (typeof browser !== 'undefined' && typeof chrome === 'undefined') {
|
|||
const STORAGE_KEY = 'verstak_queue';
|
||||
const BRIDGE_KEY = 'verstak_bridge_config';
|
||||
const RECENT_KEY = 'verstak_recent';
|
||||
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: true };
|
||||
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: false };
|
||||
const DEFAULT_DEVICE_ID = generateDeviceId();
|
||||
|
||||
// Session state
|
||||
|
|
@ -22,11 +25,10 @@ let session = {
|
|||
startedAt: null,
|
||||
};
|
||||
|
||||
// --- Lifecycle ---
|
||||
// --- Global event listeners (registered once at script load) ---
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
setupAlarms();
|
||||
setupListeners();
|
||||
// Generate a stable device ID on first install
|
||||
chrome.storage.local.get(BRIDGE_KEY, (data) => {
|
||||
const cfg = data[BRIDGE_KEY] || {};
|
||||
|
|
@ -38,25 +40,21 @@ chrome.runtime.onInstalled.addListener(() => {
|
|||
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
setupAlarms();
|
||||
setupListeners();
|
||||
});
|
||||
|
||||
function setupAlarms() {
|
||||
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
||||
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
||||
}
|
||||
// Tab tracking — global scope
|
||||
chrome.tabs.onActivated.addListener(onTabActivated);
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
||||
|
||||
function setupListeners() {
|
||||
chrome.tabs.onActivated.addListener(onTabActivated);
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
||||
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
// Alarm handler — global scope
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === 'flushEvents') flushSession();
|
||||
if (alarm.name === 'pingBridge') pingBridge();
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// Message handler — global scope
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type === 'FORCE_FLUSH') {
|
||||
flushSession();
|
||||
flushQueue();
|
||||
|
|
@ -66,8 +64,14 @@ function setupListeners() {
|
|||
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
||||
sendResponse({ ok: true });
|
||||
}
|
||||
return true; // keep channel open for async response
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// --- Alarm setup ---
|
||||
|
||||
function setupAlarms() {
|
||||
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
||||
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
||||
}
|
||||
|
||||
// --- Tab tracking ---
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
// Verstak Bridge — Background Service Worker
|
||||
// Verstak Bridge — Background Service Worker (Chrome)
|
||||
// Tracks active tab changes and queues browser events.
|
||||
// Pushes events to the Verstak HTTP bridge when available.
|
||||
//
|
||||
// IMPORTANT: All chrome.* event listeners are registered at global scope,
|
||||
// NOT inside onInstalled/onStartup. MV3 service workers can be restarted at
|
||||
// any time; listeners inside lifecycle hooks are lost on restart.
|
||||
// See: https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/events
|
||||
|
||||
const STORAGE_KEY = 'verstak_queue';
|
||||
const BRIDGE_KEY = 'verstak_bridge_config';
|
||||
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: true };
|
||||
const FLUSH_INTERVAL_MS = 30_000; // flush every 30s
|
||||
const IDLE_RESET_MS = 60_000; // reset session after 60s idle
|
||||
const RECENT_KEY = 'verstak_recent';
|
||||
const DEFAULT_CONFIG = { port: 9786, secret: '', autoGenPort: false };
|
||||
const IDLE_RESET_MS = 60_000;
|
||||
|
||||
// Session state
|
||||
let session = {
|
||||
|
|
@ -17,43 +22,55 @@ let session = {
|
|||
startedAt: null,
|
||||
};
|
||||
|
||||
// Start tracking on install
|
||||
// --- Global event listeners (registered once at script load) ---
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
setupAlarms();
|
||||
setupListeners();
|
||||
});
|
||||
|
||||
// Wake up on browser start
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
setupAlarms();
|
||||
setupListeners();
|
||||
});
|
||||
|
||||
// Tab tracking — global scope
|
||||
chrome.tabs.onActivated.addListener(onTabActivated);
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
||||
|
||||
// Alarm handler — global scope
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === 'flushEvents') flushSession();
|
||||
if (alarm.name === 'pingBridge') pingBridge();
|
||||
});
|
||||
|
||||
// Message handler — global scope
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type === 'FORCE_FLUSH') {
|
||||
flushSession();
|
||||
flushQueue();
|
||||
sendResponse({ ok: true });
|
||||
}
|
||||
if (msg.type === 'SET_TRACKING') {
|
||||
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
||||
sendResponse({ ok: true });
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// --- Alarm setup ---
|
||||
|
||||
function setupAlarms() {
|
||||
chrome.alarms.create('flushEvents', { periodInMinutes: 0.5 });
|
||||
chrome.alarms.create('pingBridge', { periodInMinutes: 1 });
|
||||
}
|
||||
|
||||
function setupListeners() {
|
||||
// Track active tab changes
|
||||
chrome.tabs.onActivated.addListener(onTabActivated);
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
chrome.windows.onFocusChanged.addListener(onWindowFocusChanged);
|
||||
// --- Tab tracking ---
|
||||
|
||||
// Listen for flush alarm
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === 'flushEvents') flushSession();
|
||||
if (alarm.name === 'pingBridge') pingBridge();
|
||||
});
|
||||
}
|
||||
|
||||
// Called when user switches tabs
|
||||
function onTabActivated(activeInfo) {
|
||||
flushSession();
|
||||
startTracking(activeInfo.tabId);
|
||||
}
|
||||
|
||||
// Called when a tab's URL or title changes
|
||||
function onTabUpdated(tabId, changeInfo, tab) {
|
||||
if (changeInfo.status === 'complete' && tab.active) {
|
||||
flushSession();
|
||||
|
|
@ -61,14 +78,11 @@ function onTabUpdated(tabId, changeInfo, tab) {
|
|||
}
|
||||
}
|
||||
|
||||
// Called when window focus changes
|
||||
function onWindowFocusChanged(windowId) {
|
||||
if (windowId === chrome.windows.WINDOW_ID_NONE) {
|
||||
// Window lost focus — flush and stop session
|
||||
flushSession();
|
||||
session = { url: '', title: '', domain: '', tabId: -1, startedAt: null };
|
||||
} else {
|
||||
// Window gained focus — get active tab and start tracking
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
if (tabs && tabs.length > 0) {
|
||||
startTracking(tabs[0].id);
|
||||
|
|
@ -86,19 +100,20 @@ function startTracking(tabId) {
|
|||
url: tab.url,
|
||||
title: tab.title || '',
|
||||
domain: extractDomain(tab.url),
|
||||
tabId: tabId,
|
||||
tabId,
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- Session flush ---
|
||||
|
||||
function flushSession() {
|
||||
if (!session.url || !session.startedAt) return;
|
||||
|
||||
const duration = Math.round((Date.now() - session.startedAt) / 1000);
|
||||
if (duration < 2) {
|
||||
// Skip very short visits
|
||||
session.startedAt = Date.now(); // reset timer
|
||||
session.startedAt = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +129,7 @@ function flushSession() {
|
|||
};
|
||||
|
||||
queueEvent(event);
|
||||
session.startedAt = Date.now(); // reset for next segment
|
||||
session.startedAt = Date.now();
|
||||
}
|
||||
|
||||
function queueEvent(event) {
|
||||
|
|
@ -122,13 +137,11 @@ function queueEvent(event) {
|
|||
const queue = data[STORAGE_KEY] || [];
|
||||
queue.push(event);
|
||||
|
||||
// Keep max 500 events in queue
|
||||
if (queue.length > 500) {
|
||||
queue.splice(0, queue.length - 500);
|
||||
}
|
||||
|
||||
chrome.storage.local.set({ [STORAGE_KEY]: queue }, () => {
|
||||
// Immediately try to flush if we have enough events
|
||||
if (queue.length >= 5) {
|
||||
flushQueue();
|
||||
}
|
||||
|
|
@ -136,6 +149,8 @@ function queueEvent(event) {
|
|||
});
|
||||
}
|
||||
|
||||
// --- Bridge communication ---
|
||||
|
||||
function flushQueue() {
|
||||
chrome.storage.local.get([STORAGE_KEY, BRIDGE_KEY], (data) => {
|
||||
const queue = data[STORAGE_KEY] || [];
|
||||
|
|
@ -161,23 +176,17 @@ function flushQueue() {
|
|||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
// Update recent events
|
||||
for (const ev of queue) {
|
||||
updateRecent(ev);
|
||||
}
|
||||
// Clear queue on success
|
||||
chrome.storage.local.set({ [STORAGE_KEY]: [] });
|
||||
// Notify popup if open
|
||||
chrome.runtime.sendMessage({ type: 'UI_UPDATE' }).catch(() => {});
|
||||
} else if (res.status === 401) {
|
||||
console.warn('[verstak] bridge auth failed, check secret');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(() => {
|
||||
// Bridge not available — keep events in queue
|
||||
if (queue.length % 10 === 0) {
|
||||
console.debug('[verstak] bridge unavailable:', err.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -202,23 +211,8 @@ function pingBridge() {
|
|||
});
|
||||
}
|
||||
|
||||
// Listen for messages from popup
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg.type === 'FORCE_FLUSH') {
|
||||
flushSession();
|
||||
flushQueue();
|
||||
sendResponse({ ok: true });
|
||||
}
|
||||
if (msg.type === 'SET_TRACKING') {
|
||||
// Tracking is always on at the background level;
|
||||
// the popup just shows the setting.
|
||||
chrome.storage.local.set({ 'verstak_tracking_enabled': msg.enabled });
|
||||
sendResponse({ ok: true });
|
||||
}
|
||||
});
|
||||
// --- Recent events ---
|
||||
|
||||
// Update recent events from flushed queue entries.
|
||||
// This is called by flushQueue on successful delivery.
|
||||
function updateRecent(event) {
|
||||
chrome.storage.local.get(RECENT_KEY, (data) => {
|
||||
const recent = data[RECENT_KEY] || [];
|
||||
|
|
@ -229,7 +223,6 @@ function updateRecent(event) {
|
|||
active_seconds: event.active_seconds,
|
||||
ts: new Date().toISOString(),
|
||||
});
|
||||
// Keep last 50
|
||||
if (recent.length > 50) {
|
||||
recent.splice(0, recent.length - 50);
|
||||
}
|
||||
|
|
@ -237,9 +230,7 @@ function updateRecent(event) {
|
|||
});
|
||||
}
|
||||
|
||||
const RECENT_KEY = 'verstak_recent';
|
||||
|
||||
// ... existing code continues below ...
|
||||
// --- Helpers ---
|
||||
|
||||
function extractDomain(url) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1541,8 +1541,9 @@
|
|||
}
|
||||
|
||||
async function acceptBrowserEvent(ev) {
|
||||
await wailsCall('DismissBrowserEvent', ev.id)
|
||||
await loadBrowserEvents()
|
||||
// Accept: mark as accepted (worklog linking comes later via suggestions)
|
||||
await wailsCall('AcceptBrowserEvent', ev.id, '')
|
||||
browserEvents = browserEvents.filter(e => e.id !== ev.id)
|
||||
}
|
||||
|
||||
async function dismissBrowserEvent(ev) {
|
||||
|
|
@ -1551,8 +1552,15 @@
|
|||
}
|
||||
|
||||
async function attachBrowserEvent(ev) {
|
||||
// Attach to currently selected node, if any
|
||||
if (selectedNode && selectedNode.id) {
|
||||
await wailsCall('AttachBrowserEventToNode', ev.id, selectedNode.id)
|
||||
browserEvents = browserEvents.filter(e => e.id !== ev.id)
|
||||
} else {
|
||||
// No node selected — just dismiss for now
|
||||
await wailsCall('DismissBrowserEvent', ev.id)
|
||||
await loadBrowserEvents()
|
||||
browserEvents = browserEvents.filter(e => e.id !== ev.id)
|
||||
}
|
||||
}
|
||||
|
||||
function writeDebugLog(msg) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type EventHandler func(events []Event)
|
|||
// Event represents a single browser event (page visit, note capture, etc.).
|
||||
type Event struct {
|
||||
ID string `json:"id"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
Type string `json:"type"` // page_visit, note_capture, screenshot
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
|
|
@ -62,7 +63,7 @@ type Config struct {
|
|||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Port: 9786,
|
||||
AutoGenPort: true,
|
||||
AutoGenPort: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,14 +77,11 @@ func GenerateSecret() string {
|
|||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// NewServer creates a bridge server. If cfg.Secret is empty, one is generated.
|
||||
// NewServer creates a bridge server.
|
||||
// If cfg.Secret is empty, no authentication is required.
|
||||
func NewServer(cfg Config, handler EventHandler) *Server {
|
||||
secret := cfg.Secret
|
||||
if secret == "" {
|
||||
secret = GenerateSecret()
|
||||
}
|
||||
return &Server{
|
||||
secret: secret,
|
||||
secret: cfg.Secret,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +217,12 @@ func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if s.handler != nil {
|
||||
// Enrich events with device_id from the batch.
|
||||
for i := range batch.Events {
|
||||
if batch.Events[i].DeviceID == "" {
|
||||
batch.Events[i].DeviceID = batch.DeviceID
|
||||
}
|
||||
}
|
||||
s.handler(batch.Events)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ type VaultAppConfig struct {
|
|||
VaultID string `json:"vault_id,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
Sync SyncSettings `json:"sync,omitempty"`
|
||||
FileWatcher bool `json:"file_watcher"`
|
||||
FileWatcher *bool `json:"file_watcher,omitempty"`
|
||||
Bridge BridgeConfig `json:"bridge,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ func DefaultAppConfig() *AppConfig {
|
|||
EnabledTemplates: []string{"folder.default", "project.default", "client.default", "document.default", "recipe.default"},
|
||||
EnabledPlugins: []string{},
|
||||
Vault: VaultAppConfig{
|
||||
FileWatcher: true,
|
||||
FileWatcher: BoolPtr(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -160,3 +160,8 @@ func DefaultVaultPath() (string, error) {
|
|||
}
|
||||
return filepath.Join(dir, "vault"), nil
|
||||
}
|
||||
|
||||
// BoolPtr returns a pointer to the given bool value.
|
||||
func BoolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,12 +236,21 @@ func (w *Watcher) handleEvent(rel, absPath string, ev fsnotify.Event) {
|
|||
}
|
||||
}
|
||||
|
||||
// New file — create record.
|
||||
// New file — check if it's already inside the vault.
|
||||
// If absPath is under vaultRoot, don't copy — just create a record.
|
||||
if isUnderVault(absPath, w.vaultRoot) {
|
||||
_, err = w.files.AddExternal(node.ID, absPath)
|
||||
if err != nil {
|
||||
log.Printf("[watcher] auto-add in-vault file %s: %v", rel, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = w.files.CopyIntoVault(node.ID, absPath, parentDir)
|
||||
if err != nil {
|
||||
log.Printf("[watcher] auto-add file %s: %v", rel, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.logActivity(node.ID, activity.TypeFileAdded, fi.Name(), rel)
|
||||
|
||||
case ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename):
|
||||
|
|
@ -342,3 +351,10 @@ func hashFileFast(absPath string) (string, int64) {
|
|||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), n
|
||||
}
|
||||
|
||||
// isUnderVault reports whether absPath is inside vaultRoot.
|
||||
func isUnderVault(absPath, vaultRoot string) bool {
|
||||
absPath, _ = filepath.Abs(absPath)
|
||||
vaultRoot, _ = filepath.Abs(vaultRoot)
|
||||
return strings.HasPrefix(absPath, vaultRoot+string(filepath.Separator)) || absPath == vaultRoot
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue