Skip to content

Capture page DOM and screenshot, save to OPFS, backup to S3 #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
72 changes: 0 additions & 72 deletions background.js

This file was deleted.

File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions manifest.json → public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
"optional_host_permissions": [
"*://*\/*"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"icons": {
"16": "16.png",
"32": "32.png",
Expand Down
76 changes: 75 additions & 1 deletion options.html → public/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
<li class="nav-item">
<a class="nav-link" id="import-tab" data-bs-toggle="tab" href="#import" role="tab">📤 Bulk Import URLs</a>
</li>
<li class="nav-item">
<a class="nav-link" id="s3-tab" data-bs-toggle="tab" href="#s3" role="tab">🗄️ S3 Configuration</a>
</li>
</ul>

<div class="tab-content">
Expand Down Expand Up @@ -469,6 +472,76 @@ <h5 class="mb-0">Import Browser Cookies to Archiving Profile</h5>
</div>
</div>
</div>

<!-- S3 Configuration Tab -->
<div class="tab-pane fade" id="s3" role="tabpanel">
<div class="row mt-4">
<div class="col-md-8">
<form id="s3Form" class="needs-validation" novalidate>
<div class="mb-3">
<label for="s3_endpoint" class="form-label"><b>🌐 S3 Endpoint URL *</b></label>
<input type="url" class="form-control" id="s3_endpoint"
placeholder="https://s3.amazonaws.com">
<div class="form-text">
The endpoint URL for your S3 service. For AWS S3, use the regional endpoint (e.g., <code>https://s3.us-west-2.amazonaws.com</code>).
For other S3-compatible services, use their specific endpoint.
</div>
</div>

<div class="mb-3">
<label for="s3_region" class="form-label"><b>🌍 S3 Region *</b></label>
<input type="text" class="form-control" id="s3_region"
placeholder="us-east-1">
<div class="form-text">
The region where your S3 bucket is located (e.g., <code>us-east-1</code>, <code>eu-west-1</code>).
</div>
</div>

<div class="mb-3">
<label for="s3_bucket" class="form-label"><b>🪣 S3 Bucket Name *</b></label>
<input type="text" class="form-control" id="s3_bucket"
placeholder="my-archive-bucket">
<div class="form-text">
The name of the S3 bucket where screenshots and archives will be stored.
</div>
</div>

<div class="mb-3">
<label for="s3_access_key_id" class="form-label"><b>🔑 Access Key ID *</b></label>
<input type="text" class="form-control" id="s3_access_key_id"
placeholder="AKIAIOSFODNN7EXAMPLE">
<div class="form-text">
Your S3 access key ID. It's recommended to use a key with limited permissions.
</div>
</div>

<div class="mb-3">
<label for="s3_secret_access_key" class="form-label"><b>🔒 Secret Access Key *</b></label>
<input type="password" class="form-control" id="s3_secret_access_key"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">
<div class="form-text">
Your S3 secret access key. This will be stored securely in your browser.
</div>
</div>

<div class="d-flex gap-2">
<button type="button" id="saveS3Config" class="btn btn-primary">
Save Configuration
<span class="status-indicator" id="s3ConfigStatus"></span>
</button>
<button type="button" id="testS3Credentials" class="btn btn-outline-secondary">
Test S3 Connection
<span class="status-indicator" id="s3TestStatus"></span>
</button>
</div>
<div class="form-text mt-2">
<span id="s3ConfigStatusText" class="ms-2"></span>
<span id="s3TestStatusText" class="ms-2"></span>
</div>
</form>
</div>
</div>
</div>
</div>

<footer>
Expand Down Expand Up @@ -507,7 +580,8 @@ <h5 class="mb-0">Import Browser Cookies to Archiving Profile</h5>
<!-- <script type="module" src="entries-tab.js"></script>
<script type="module" src="import-tab.js"></script>
<script type="module" src="personas-tab.js"></script>
<script type="module" src="cookies-tab.js"></script> -->
<script type="module" src="cookies-tab.js"></script>
<script type="module" src="s3-tab.js"></script> -->
<script type="module" src="options.js"></script>
</body>
</html>
161 changes: 161 additions & 0 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// background.js

import {
addToArchiveBox,
captureScreenshot,
captureDom,
uploadToS3,
readFileFromOPFS,
} from "./utils.js";

chrome.runtime.onInstalled.addListener(function () {
chrome.contextMenus.create({
id: 'save_to_archivebox_ctxmenu',
title: 'Save to ArchiveBox',
});
});


chrome.runtime.onMessage.addListener(async (message) => {
const options_url = chrome.runtime.getURL('options.html') + `?search=${message.id}`;
console.log('i ArchiveBox Collector showing options.html', options_url);
if (message.action === 'openOptionsPage') {
await chrome.tabs.create({ url: options_url });
}
});


// Listeners for user-submitted actions

async function saveEntry(tab) {
const entry = {
id: crypto.randomUUID(),
url: tab.url,
timestamp: new Date().toISOString(),
tags: [],
title: tab.title,
favicon: tab.favIconUrl
};

// Save the entry first
const { entries = [] } = await chrome.storage.local.get('entries');
entries.push(entry);
await chrome.storage.local.set({ entries });

// Inject scripts - CSS now handled in popup.js
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['popup.js']
});
}

chrome.action.onClicked.addListener((tab, data) => saveEntry(tab));

chrome.contextMenus.onClicked.addListener((info, tab) => saveEntry(tab));

// Listeners for messages from other workers and contexts

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'archivebox_add') {
addToArchiveBox(message.body, sendResponse, sendResponse);
}
return true;
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'capture_screenshot') {
(async ()=> {
try {
const result = await captureScreenshot(message.timestamp);
if (result.ok) {
sendResponse(result);
} else {
throw new Error(result.errorMessage);
}
} catch (error) {
console.log("Failed to capture screenshot: ", error);
sendResponse({ok: false, errorMessage: String(error)});
}
})();

return true;
}
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'capture_dom') {
(async ()=> {
try {
const result = await captureDom(message.timestamp)
if (result.ok) {
sendResponse(result);
} else {
throw new Error(result.errorMessage);
}
} catch (error) {
console.log("Failed to capture DOM: ", error);
sendResponse({ok: false, errorMessage: String(error)});
}
})();

return true;
}
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'save_to_s3') {
(async ()=> {
try {
const data = await readFileFromOPFS(message.path);

if (!data) {
throw new Error('Failed to read file from OPFS');
}

const fileName = message.path.split('/').filter(part=>part.length > 0).pop();

console.log('filename: ', fileName);
const s3Url = await uploadToS3(fileName, data, message.contentType);
sendResponse({ok: true, url: s3Url});
} catch (error) {
console.log('Failed to upload to S3: ', error);
sendResponse({ok: false, errorMessage: String(error)});
}
})();
return true;
}
})


chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'test_s3') {
(async () => {
// Upload test file
try {
const fileName = `.connection_test_${Date.now()}.txt`;
const randomContent = Math.random().toString(36).substring(2, 15);
const testData = new TextEncoder().encode(randomContent);

const s3Url = await uploadToS3(fileName, testData, 'text/plain');

// Verify test file matches
const response = await fetch(s3Url);

if (response.ok) {
const responseText = await response.text();
const testPassed = responseText === randomContent;
sendResponse(testPassed ? 'success' : 'failure');
} else {
console.error(`Failed to fetch test content: ${response.status} ${response,statusText}`);
sendResponse('failure');
}
} catch (error) {
console.error('S3 credential test failed:', error);
sendResponse('failure');
}
})();
return true;
}
});


File renamed without changes.
12 changes: 12 additions & 0 deletions src/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'capture_dom') {
try {
const domContent = document.documentElement.outerHTML;
sendResponse({domContent: domContent})
} catch {
console.log("failed to download", chrome.runtime.lastError);
throw new Error(`failed to download: ${chrome.runtime.lastError}`);
}
}
});

File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions options.js → src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { initializeImport } from './import-tab.js';
import { initializePersonasTab } from './personas-tab.js';
import { initializeCookiesTab } from './cookies-tab.js';
import { initializeConfigTab } from './config-tab.js';
import { initializeS3Tab } from './s3-tab.js';

// Initialize all tabs when options page loads
document.addEventListener('DOMContentLoaded', () => {
Expand All @@ -11,6 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
initializePersonasTab();
initializeCookiesTab();
initializeConfigTab();
initializeS3Tab();

function changeTab() {
if (window.location.hash && window.location.hash !== document.querySelector('a.nav-link.active').id) {
Expand Down
File renamed without changes.
Loading