WIP: Debug backup file discovery system - Fixed recursive directory search in CapacitorPlatformService to properly search subdirectories instead of excluding them - Added missing listFilesInDirectory method to all platform services for directory browsing functionality - Added debug methods (debugTimeSafariDirectory, createTestBackupFile, testDirectoryContexts) to help diagnose file visibility issues - Enhanced logging for backup file discovery process - Current issue: TimeSafari directory exists in Download but shows 'Directory does not exist' when trying to read contents - Need to investigate why JSON backup files are not being found despite directory existence
This commit is contained in:
533
CONTACT_BACKUP_SYSTEM.md
Normal file
533
CONTACT_BACKUP_SYSTEM.md
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
# TimeSafari Contact Backup System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The TimeSafari application implements a comprehensive contact backup and listing system that works across multiple platforms (Web, iOS, Android, Desktop). This document breaks down how contacts are saved, exported, and listed as backups.
|
||||||
|
|
||||||
|
## Architecture Components
|
||||||
|
|
||||||
|
### 1. Database Layer
|
||||||
|
|
||||||
|
#### Contact Data Structure
|
||||||
|
```typescript
|
||||||
|
interface Contact {
|
||||||
|
did: string; // Decentralized Identifier (primary key)
|
||||||
|
contactMethods?: ContactMethod[]; // Array of contact methods (EMAIL, SMS, etc.)
|
||||||
|
name?: string; // Display name
|
||||||
|
nextPubKeyHashB64?: string; // Base64 hash of next public key
|
||||||
|
notes?: string; // User notes
|
||||||
|
profileImageUrl?: string; // Profile image URL
|
||||||
|
publicKeyBase64?: string; // Base64 encoded public key
|
||||||
|
seesMe?: boolean; // Visibility setting
|
||||||
|
registered?: boolean; // Registration status
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContactMethod {
|
||||||
|
label: string; // Display label
|
||||||
|
type: string; // Type (EMAIL, SMS, WHATSAPP, etc.)
|
||||||
|
value: string; // Contact value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Database Schema
|
||||||
|
```sql
|
||||||
|
CREATE TABLE contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
did TEXT NOT NULL, -- Decentralized Identifier
|
||||||
|
name TEXT, -- Display name
|
||||||
|
contactMethods TEXT, -- JSON string of contact methods
|
||||||
|
nextPubKeyHashB64 TEXT, -- Next public key hash
|
||||||
|
notes TEXT, -- User notes
|
||||||
|
profileImageUrl TEXT, -- Profile image URL
|
||||||
|
publicKeyBase64 TEXT, -- Public key
|
||||||
|
seesMe BOOLEAN, -- Visibility flag
|
||||||
|
registered BOOLEAN -- Registration status
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||||
|
CREATE INDEX idx_contacts_name ON contacts(name);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Contact Saving Operations
|
||||||
|
|
||||||
|
#### A. Adding New Contacts
|
||||||
|
|
||||||
|
**1. QR Code Scanning (`ContactQRScanFullView.vue`)**
|
||||||
|
```typescript
|
||||||
|
async addNewContact(contact: Contact) {
|
||||||
|
// Check for existing contact
|
||||||
|
const existingContacts = await platformService.dbQuery(
|
||||||
|
"SELECT * FROM contacts WHERE did = ?", [contact.did]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingContact) {
|
||||||
|
// Handle duplicate
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert contactMethods to JSON string for storage
|
||||||
|
contact.contactMethods = JSON.stringify(
|
||||||
|
parseJsonField(contact.contactMethods, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert into database
|
||||||
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
|
contact as unknown as Record<string, unknown>, "contacts"
|
||||||
|
);
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Manual Contact Addition (`ContactsView.vue`)**
|
||||||
|
```typescript
|
||||||
|
private async addContact(newContact: Contact) {
|
||||||
|
// Validate DID format
|
||||||
|
if (!isDid(newContact.did)) {
|
||||||
|
throw new Error("Invalid DID format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and execute INSERT statement
|
||||||
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
|
newContact as unknown as Record<string, unknown>, "contacts"
|
||||||
|
);
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Contact Import (`ContactImportView.vue`)**
|
||||||
|
```typescript
|
||||||
|
async importContacts() {
|
||||||
|
for (const contact of selectedContacts) {
|
||||||
|
const contactToStore = contactToDbRecord(contact);
|
||||||
|
|
||||||
|
if (existingContact) {
|
||||||
|
// Update existing contact
|
||||||
|
const { sql, params } = databaseUtil.generateUpdateStatement(
|
||||||
|
contactToStore, "contacts", "did = ?", [contact.did]
|
||||||
|
);
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
} else {
|
||||||
|
// Add new contact
|
||||||
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
|
contactToStore, "contacts"
|
||||||
|
);
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Updating Existing Contacts
|
||||||
|
|
||||||
|
**Contact Editing (`ContactEditView.vue`)**
|
||||||
|
```typescript
|
||||||
|
async saveEdit() {
|
||||||
|
// Normalize contact methods
|
||||||
|
const contactMethods = this.contactMethods.map(method => ({
|
||||||
|
...method,
|
||||||
|
type: method.type.toUpperCase()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
const contactMethodsString = JSON.stringify(contactMethods);
|
||||||
|
await platformService.dbExec(
|
||||||
|
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?",
|
||||||
|
[this.contactName, this.contactNotes, contactMethodsString, this.contact?.did]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Contact Export/Backup System
|
||||||
|
|
||||||
|
#### A. Export Process (`DataExportSection.vue`)
|
||||||
|
|
||||||
|
#### 1. Data Retrieval
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async exportDatabase() {
|
||||||
|
// Query all contacts from database
|
||||||
|
const result = await platformService.dbQuery("SELECT * FROM contacts");
|
||||||
|
const allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[];
|
||||||
|
|
||||||
|
// Convert to export format
|
||||||
|
const exportData = contactsToExportJson(allContacts);
|
||||||
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Export Format Conversion (`libs/util.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||||
|
const rows = contacts.map((contact) => ({
|
||||||
|
did: contact.did,
|
||||||
|
name: contact.name || null,
|
||||||
|
contactMethods: contact.contactMethods
|
||||||
|
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
||||||
|
: null,
|
||||||
|
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||||
|
notes: contact.notes || null,
|
||||||
|
profileImageUrl: contact.profileImageUrl || null,
|
||||||
|
publicKeyBase64: contact.publicKeyBase64 || null,
|
||||||
|
seesMe: contact.seesMe || false,
|
||||||
|
registered: contact.registered || false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: [{ tableName: "contacts", rows }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. File Generation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create timestamped filename
|
||||||
|
const timestamp = getTimestampForFilename();
|
||||||
|
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`;
|
||||||
|
|
||||||
|
// Create blob and save
|
||||||
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Platform-Specific File Saving
|
||||||
|
|
||||||
|
##### 1. Web Platform (`WebPlatformService.ts`)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Uses browser download API
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||||
|
downloadAnchor.href = downloadUrl;
|
||||||
|
downloadAnchor.download = fileName;
|
||||||
|
downloadAnchor.click();
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Mobile Platforms (`CapacitorPlatformService.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async writeAndShareFile(fileName: string, content: string, options = {}) {
|
||||||
|
let fileUri: string;
|
||||||
|
|
||||||
|
if (options.allowLocationSelection) {
|
||||||
|
// User chooses location
|
||||||
|
fileUri = await this.saveWithUserChoice(fileName, content, options.mimeType);
|
||||||
|
} else if (options.saveToPrivateStorage) {
|
||||||
|
// Save to app-private storage
|
||||||
|
const result = await Filesystem.writeFile({
|
||||||
|
path: fileName,
|
||||||
|
data: content,
|
||||||
|
directory: Directory.Data,
|
||||||
|
encoding: Encoding.UTF8,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
fileUri = result.uri;
|
||||||
|
} else {
|
||||||
|
// Save to user-accessible location (Downloads/Documents)
|
||||||
|
fileUri = await this.saveToDownloads(fileName, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share the file
|
||||||
|
return await this.shareFile(fileUri, fileName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 3. Desktop Platforms (`ElectronPlatformService.ts`, `PyWebViewPlatformService.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Not implemented - returns empty results
|
||||||
|
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Backup File Listing System
|
||||||
|
|
||||||
|
#### A. File Discovery (`CapacitorPlatformService.ts`)
|
||||||
|
|
||||||
|
##### 1. Enhanced File Discovery
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async listUserAccessibleFilesEnhanced(): Promise<Array<{name: string, uri: string, size?: number, path?: string}>> {
|
||||||
|
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = [];
|
||||||
|
|
||||||
|
if (this.getCapabilities().isIOS) {
|
||||||
|
// iOS: Documents directory
|
||||||
|
const result = await Filesystem.readdir({
|
||||||
|
path: ".",
|
||||||
|
directory: Directory.Documents,
|
||||||
|
});
|
||||||
|
const files = result.files.map((file) => ({
|
||||||
|
name: typeof file === "string" ? file : file.name,
|
||||||
|
uri: `file://${file.uri || file}`,
|
||||||
|
size: typeof file === "string" ? undefined : file.size,
|
||||||
|
path: "Documents"
|
||||||
|
}));
|
||||||
|
allFiles.push(...files);
|
||||||
|
} else {
|
||||||
|
// Android: Multiple locations
|
||||||
|
const commonPaths = ["Download", "Documents", "Backups", "TimeSafari", "Data"];
|
||||||
|
|
||||||
|
for (const path of commonPaths) {
|
||||||
|
try {
|
||||||
|
const result = await Filesystem.readdir({
|
||||||
|
path: path,
|
||||||
|
directory: Directory.ExternalStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter for TimeSafari-related files
|
||||||
|
const relevantFiles = result.files
|
||||||
|
.filter(file => {
|
||||||
|
const fileName = typeof file === "string" ? file : file.name;
|
||||||
|
const name = fileName.toLowerCase();
|
||||||
|
return name.includes('timesafari') ||
|
||||||
|
name.includes('backup') ||
|
||||||
|
name.includes('contacts') ||
|
||||||
|
name.endsWith('.json');
|
||||||
|
})
|
||||||
|
.map((file) => ({
|
||||||
|
name: typeof file === "string" ? file : file.name,
|
||||||
|
uri: `file://${file.uri || file}`,
|
||||||
|
size: typeof file === "string" ? undefined : file.size,
|
||||||
|
path: path
|
||||||
|
}));
|
||||||
|
|
||||||
|
allFiles.push(...relevantFiles);
|
||||||
|
} catch (error) {
|
||||||
|
// Silently skip inaccessible directories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allFiles;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Backup File Filtering**
|
||||||
|
```typescript
|
||||||
|
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
||||||
|
const allFiles = await this.listUserAccessibleFilesEnhanced();
|
||||||
|
|
||||||
|
const backupFiles = allFiles
|
||||||
|
.filter(file => {
|
||||||
|
const name = file.name.toLowerCase();
|
||||||
|
|
||||||
|
// Exclude directory-access notification files
|
||||||
|
if (name.startsWith('timesafari-directory-access-') && name.endsWith('.txt')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check backup criteria
|
||||||
|
const isJson = name.endsWith('.json');
|
||||||
|
const hasTimeSafari = name.includes('timesafari');
|
||||||
|
const hasBackup = name.includes('backup');
|
||||||
|
const hasContacts = name.includes('contacts');
|
||||||
|
const hasSeed = name.includes('seed');
|
||||||
|
const hasExport = name.includes('export');
|
||||||
|
const hasData = name.includes('data');
|
||||||
|
|
||||||
|
return isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData;
|
||||||
|
})
|
||||||
|
.map(file => {
|
||||||
|
const name = file.name.toLowerCase();
|
||||||
|
let type: 'contacts' | 'seed' | 'other' = 'other';
|
||||||
|
|
||||||
|
// Categorize files
|
||||||
|
if (name.includes('contacts') || (name.includes('timesafari') && name.includes('backup'))) {
|
||||||
|
type = 'contacts';
|
||||||
|
} else if (name.includes('seed') || name.includes('mnemonic') || name.includes('private')) {
|
||||||
|
type = 'seed';
|
||||||
|
} else if (name.endsWith('.json')) {
|
||||||
|
type = 'other';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...file, type };
|
||||||
|
});
|
||||||
|
|
||||||
|
return backupFiles;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. UI Components (`BackupFilesList.vue`)
|
||||||
|
|
||||||
|
**1. File Display**
|
||||||
|
```typescript
|
||||||
|
@Component
|
||||||
|
export default class BackupFilesList extends Vue {
|
||||||
|
backupFiles: Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}> = [];
|
||||||
|
selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all';
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
async refreshFiles() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
this.backupFiles = await this.platformService.listBackupFiles();
|
||||||
|
|
||||||
|
// Log file type distribution
|
||||||
|
const typeCounts = {
|
||||||
|
contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
|
||||||
|
seed: this.backupFiles.filter(f => f.type === 'seed').length,
|
||||||
|
other: this.backupFiles.filter(f => f.type === 'other').length,
|
||||||
|
total: this.backupFiles.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. File Operations**
|
||||||
|
```typescript
|
||||||
|
async openFile(fileUri: string, fileName: string) {
|
||||||
|
const result = await this.platformService.openFile(fileUri, fileName);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Failed to open file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openBackupDirectory() {
|
||||||
|
const result = await this.platformService.openBackupDirectory();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Failed to open backup directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Platform-Specific Storage Locations
|
||||||
|
|
||||||
|
#### A. iOS Platform
|
||||||
|
- **Primary Location**: Documents folder (accessible via Files app)
|
||||||
|
- **Persistence**: Survives app installations
|
||||||
|
- **Access**: Through iOS Files app
|
||||||
|
- **File Format**: JSON with timestamped filenames
|
||||||
|
|
||||||
|
#### B. Android Platform
|
||||||
|
- **Primary Locations**:
|
||||||
|
- `Download/TimeSafari/` (external storage)
|
||||||
|
- `TimeSafari/` (external storage)
|
||||||
|
- User-chosen locations via file picker
|
||||||
|
- **Persistence**: Survives app installations
|
||||||
|
- **Access**: Through file managers
|
||||||
|
- **File Format**: JSON with timestamped filenames
|
||||||
|
|
||||||
|
#### C. Web Platform
|
||||||
|
- **Primary Location**: Browser downloads folder
|
||||||
|
- **Persistence**: Depends on browser settings
|
||||||
|
- **Access**: Through browser download manager
|
||||||
|
- **File Format**: JSON with timestamped filenames
|
||||||
|
|
||||||
|
#### D. Desktop Platforms (Electron/PyWebView)
|
||||||
|
- **Status**: Not implemented
|
||||||
|
- **Fallback**: Returns empty arrays for file operations
|
||||||
|
|
||||||
|
### 6. File Naming Convention
|
||||||
|
|
||||||
|
#### A. Contact Backup Files
|
||||||
|
```
|
||||||
|
TimeSafari-backup-contacts-YYYY-MM-DD-HH-MM-SS.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. File Content Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"tableName": "contacts",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"did": "did:ethr:0x...",
|
||||||
|
"name": "Contact Name",
|
||||||
|
"contactMethods": "[{\"type\":\"EMAIL\",\"value\":\"email@example.com\"}]",
|
||||||
|
"notes": "User notes",
|
||||||
|
"profileImageUrl": "https://...",
|
||||||
|
"publicKeyBase64": "base64...",
|
||||||
|
"seesMe": true,
|
||||||
|
"registered": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Error Handling and Logging
|
||||||
|
|
||||||
|
#### A. Comprehensive Logging
|
||||||
|
```typescript
|
||||||
|
logger.log("[CapacitorPlatformService] File write successful:", {
|
||||||
|
uri: fileUri,
|
||||||
|
saved,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("[BackupFilesList] Refreshed backup files:", {
|
||||||
|
count: this.backupFiles.length,
|
||||||
|
files: this.backupFiles.map(f => ({
|
||||||
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
path: f.path,
|
||||||
|
size: f.size
|
||||||
|
})),
|
||||||
|
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Error Recovery
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
// File operations
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[CapacitorPlatformService] Failed to list backup files:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Security Considerations
|
||||||
|
|
||||||
|
#### A. Data Privacy
|
||||||
|
- Contact data is stored locally on device
|
||||||
|
- No cloud synchronization of contact data
|
||||||
|
- User controls visibility settings per contact
|
||||||
|
- Backup files contain only user-authorized data
|
||||||
|
|
||||||
|
#### B. File Access
|
||||||
|
- Platform-specific permission handling
|
||||||
|
- User choice for file locations
|
||||||
|
- Secure storage options for sensitive data
|
||||||
|
- Proper error handling for access failures
|
||||||
|
|
||||||
|
### 9. Performance Optimizations
|
||||||
|
|
||||||
|
#### A. Database Operations
|
||||||
|
- Indexed queries on `did` and `name` fields
|
||||||
|
- Batch operations for multiple contacts
|
||||||
|
- Efficient JSON serialization/deserialization
|
||||||
|
- Connection pooling and reuse
|
||||||
|
|
||||||
|
#### B. File Operations
|
||||||
|
- Asynchronous file I/O
|
||||||
|
- Efficient file discovery algorithms
|
||||||
|
- Caching of file lists
|
||||||
|
- Background refresh operations
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The TimeSafari contact backup system provides:
|
||||||
|
|
||||||
|
1. **Robust Data Storage**: SQLite-based contact storage with proper indexing
|
||||||
|
2. **Cross-Platform Compatibility**: Works on web, iOS, Android, and desktop
|
||||||
|
3. **Flexible Export Options**: Multiple file formats and storage locations
|
||||||
|
4. **Intelligent File Discovery**: Finds backup files regardless of user-chosen locations
|
||||||
|
5. **User-Friendly Interface**: Clear categorization and easy file management
|
||||||
|
6. **Comprehensive Logging**: Detailed tracking for debugging and monitoring
|
||||||
|
7. **Security-First Design**: Privacy-preserving with user-controlled data access
|
||||||
|
|
||||||
|
The system ensures that users can reliably backup and restore their contact data across different platforms while maintaining data integrity and user privacy.
|
||||||
@@ -51,6 +51,20 @@
|
|||||||
<font-awesome icon="bug" class="fa-fw" />
|
<font-awesome icon="bug" class="fa-fw" />
|
||||||
Debug
|
Debug
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click="createTestBackup"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="px-3 py-1 bg-green-500 text-white rounded text-sm hover:bg-green-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Create Test Backup
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="testDirectoryContexts"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="px-3 py-1 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Test Contexts
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,21 +252,57 @@ export default class BackupFilesList extends Vue {
|
|||||||
*/
|
*/
|
||||||
debugShowAll = false;
|
debugShowAll = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and requests storage permissions if needed.
|
||||||
|
* Returns true if permission is granted, false otherwise.
|
||||||
|
*/
|
||||||
|
private async ensureStoragePermission(): Promise<boolean> {
|
||||||
|
logger.log('[BackupFilesList] ensureStoragePermission called. platformCapabilities:', this.platformCapabilities);
|
||||||
|
if (!this.platformCapabilities.hasFileSystem) return true;
|
||||||
|
// Only relevant for native platforms (Android/iOS)
|
||||||
|
const platformService = this.platformService as any;
|
||||||
|
if (typeof platformService.checkStoragePermissions === 'function') {
|
||||||
|
try {
|
||||||
|
await platformService.checkStoragePermissions();
|
||||||
|
logger.log('[BackupFilesList] Storage permission granted.');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupFilesList] Storage permission denied:', error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Storage Permission Required",
|
||||||
|
text: "This app needs permission to access your files to list and restore backups. Please grant storage permission and try again.",
|
||||||
|
},
|
||||||
|
7000,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle hook to load backup files when component is mounted
|
* Lifecycle hook to load backup files when component is mounted
|
||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
logger.log('[BackupFilesList] mounted hook called. platformCapabilities:', this.platformCapabilities);
|
||||||
if (this.platformCapabilities.hasFileSystem) {
|
if (this.platformCapabilities.hasFileSystem) {
|
||||||
// Set default root path
|
// Check/request permission before loading
|
||||||
if (this.platformCapabilities.isIOS) {
|
const hasPermission = await this.ensureStoragePermission();
|
||||||
this.currentPath = ['.'];
|
if (hasPermission) {
|
||||||
} else {
|
// Set default root path
|
||||||
this.currentPath = ['Download', 'TimeSafari'];
|
if (this.platformCapabilities.isIOS) {
|
||||||
|
this.currentPath = ['.'];
|
||||||
|
} else {
|
||||||
|
this.currentPath = ['Download', 'TimeSafari'];
|
||||||
|
}
|
||||||
|
await this.loadDirectory();
|
||||||
|
this.refreshInterval = window.setInterval(() => {
|
||||||
|
this.loadDirectory();
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
}
|
}
|
||||||
await this.loadDirectory();
|
|
||||||
this.refreshInterval = window.setInterval(() => {
|
|
||||||
this.loadDirectory();
|
|
||||||
}, 5 * 60 * 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,12 +318,16 @@ export default class BackupFilesList extends Vue {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for filtered files based on selected type
|
* Computed property for filtered files based on selected type
|
||||||
|
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
|
||||||
*/
|
*/
|
||||||
get filteredFiles() {
|
get filteredFiles() {
|
||||||
if (this.selectedType === 'all') {
|
if (this.selectedType === 'all') {
|
||||||
|
logger.log('[BackupFilesList] filteredFiles (All):', this.backupFiles);
|
||||||
return this.backupFiles;
|
return this.backupFiles;
|
||||||
}
|
}
|
||||||
return this.backupFiles.filter(file => file.type === this.selectedType);
|
const filtered = this.backupFiles.filter(file => file.type === this.selectedType);
|
||||||
|
logger.log(`[BackupFilesList] filteredFiles (${this.selectedType}):`, filtered);
|
||||||
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -348,15 +402,21 @@ export default class BackupFilesList extends Vue {
|
|||||||
* Refreshes the list of backup files from the device
|
* Refreshes the list of backup files from the device
|
||||||
*/
|
*/
|
||||||
async refreshFiles() {
|
async refreshFiles() {
|
||||||
|
logger.log('[BackupFilesList] refreshFiles called.');
|
||||||
if (!this.platformCapabilities.hasFileSystem) {
|
if (!this.platformCapabilities.hasFileSystem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Check/request permission before refreshing
|
||||||
|
const hasPermission = await this.ensureStoragePermission();
|
||||||
|
if (!hasPermission) {
|
||||||
|
this.backupFiles = [];
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
try {
|
||||||
this.backupFiles = await this.platformService.listBackupFiles();
|
this.backupFiles = await this.platformService.listBackupFiles();
|
||||||
|
logger.log('[BackupFilesList] Refreshed backup files:', {
|
||||||
logger.log("[BackupFilesList] Refreshed backup files:", {
|
|
||||||
count: this.backupFiles.length,
|
count: this.backupFiles.length,
|
||||||
files: this.backupFiles.map(f => ({
|
files: this.backupFiles.map(f => ({
|
||||||
name: f.name,
|
name: f.name,
|
||||||
@@ -367,7 +427,6 @@ export default class BackupFilesList extends Vue {
|
|||||||
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
|
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug: Log file type distribution
|
// Debug: Log file type distribution
|
||||||
const typeCounts = {
|
const typeCounts = {
|
||||||
contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
|
contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
|
||||||
@@ -375,9 +434,11 @@ export default class BackupFilesList extends Vue {
|
|||||||
other: this.backupFiles.filter(f => f.type === 'other').length,
|
other: this.backupFiles.filter(f => f.type === 'other').length,
|
||||||
total: this.backupFiles.length
|
total: this.backupFiles.length
|
||||||
};
|
};
|
||||||
logger.log("[BackupFilesList] File type distribution:", typeCounts);
|
logger.log('[BackupFilesList] File type distribution:', typeCounts);
|
||||||
|
// Log the full backupFiles array for debugging the 'All' tab count
|
||||||
|
logger.log('[BackupFilesList] backupFiles array for All tab:', this.backupFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[BackupFilesList] Failed to refresh backup files:", error);
|
logger.error('[BackupFilesList] Failed to refresh backup files:', error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -393,11 +454,103 @@ export default class BackupFilesList extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public method to refresh files from external components
|
* Creates a test backup file for debugging purposes
|
||||||
* Used by DataExportSection to refresh after saving new files
|
|
||||||
*/
|
*/
|
||||||
public async refreshAfterSave() {
|
async createTestBackup() {
|
||||||
logger.log("[BackupFilesList] Refreshing files after save operation");
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
logger.log('[BackupFilesList] Creating test backup file');
|
||||||
|
|
||||||
|
const result = await this.platformService.createTestBackupFile();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
logger.log('[BackupFilesList] Test backup file created successfully:', {
|
||||||
|
fileName: result.fileName,
|
||||||
|
uri: result.uri,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Test Backup Created",
|
||||||
|
text: `Test backup file "${result.fileName}" created successfully. Refresh the list to see it.`,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the file list to show the new test file
|
||||||
|
await this.refreshFiles();
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || "Failed to create test backup file");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupFilesList] Failed to create test backup file:', error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Test Backup Failed",
|
||||||
|
text: "Failed to create test backup file. Check the console for details.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests different directory contexts to debug file visibility issues
|
||||||
|
*/
|
||||||
|
async testDirectoryContexts() {
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
logger.log('[BackupFilesList] Testing directory contexts');
|
||||||
|
|
||||||
|
const debugOutput = await this.platformService.testDirectoryContexts();
|
||||||
|
|
||||||
|
logger.log('[BackupFilesList] Directory context test results:', debugOutput);
|
||||||
|
|
||||||
|
// Show the debug output in a notification or alert
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Directory Context Test",
|
||||||
|
text: "Directory context test completed. Check the console for detailed results.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also log the full output to console for easy access
|
||||||
|
console.log("=== Directory Context Test Results ===");
|
||||||
|
console.log(debugOutput);
|
||||||
|
console.log("=== End Test Results ===");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[BackupFilesList] Failed to test directory contexts:', error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Context Test Failed",
|
||||||
|
text: "Failed to test directory contexts. Check the console for details.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the file list after a backup is created
|
||||||
|
* This method can be called from parent components
|
||||||
|
*/
|
||||||
|
async refreshAfterSave() {
|
||||||
|
logger.log('[BackupFilesList] refreshAfterSave called');
|
||||||
await this.refreshFiles();
|
await this.refreshFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,14 +615,18 @@ export default class BackupFilesList extends Vue {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the count of files for a specific type
|
* Gets the count of files for a specific type
|
||||||
* @param type - File type to count
|
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
|
||||||
* @returns Number of files of the specified type
|
|
||||||
*/
|
*/
|
||||||
getFileCountByType(type: 'all' | 'contacts' | 'seed' | 'other'): number {
|
getFileCountByType(type: 'all' | 'contacts' | 'seed' | 'other'): number {
|
||||||
|
let count;
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
return this.backupFiles.length;
|
count = this.backupFiles.length;
|
||||||
|
logger.log('[BackupFilesList] getFileCountByType (All):', count, this.backupFiles);
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
return this.backupFiles.filter(file => file.type === type).length;
|
count = this.backupFiles.filter(file => file.type === type).length;
|
||||||
|
logger.log(`[BackupFilesList] getFileCountByType (${type}):`, count);
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -214,4 +214,33 @@ export interface PlatformService {
|
|||||||
* @returns Promise resolving to success status
|
* @returns Promise resolving to success status
|
||||||
*/
|
*/
|
||||||
openBackupDirectory(): Promise<{ success: boolean; error?: string }>;
|
openBackupDirectory(): Promise<{ success: boolean; error?: string }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a test backup file to verify file writing and reading functionality.
|
||||||
|
* This is useful for debugging file visibility issues.
|
||||||
|
* @returns Promise resolving to success status and file information
|
||||||
|
*/
|
||||||
|
createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests different directory contexts to see what files are available.
|
||||||
|
* This helps debug file visibility issues across different storage contexts.
|
||||||
|
* @returns Promise resolving to debug information about file discovery across contexts
|
||||||
|
*/
|
||||||
|
testDirectoryContexts(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists files and folders in a specific directory for directory browsing
|
||||||
|
* @param path - The directory path to list
|
||||||
|
* @param debugShowAll - Debug flag to treat all entries as files
|
||||||
|
* @returns Promise resolving to array of directory entries
|
||||||
|
*/
|
||||||
|
listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to check what's actually in the TimeSafari directory
|
||||||
|
* This helps identify if the directory exists but is empty or has permission issues
|
||||||
|
* @returns Promise resolving to debug information about the TimeSafari directory
|
||||||
|
*/
|
||||||
|
debugTimeSafariDirectory(): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1770,15 +1770,11 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = [];
|
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = [];
|
||||||
|
|
||||||
if (this.getCapabilities().isIOS) {
|
if (this.getCapabilities().isIOS) {
|
||||||
// iOS: List files in Documents directory
|
// iOS: Documents directory
|
||||||
const result = await Filesystem.readdir({
|
const result = await Filesystem.readdir({
|
||||||
path: ".",
|
path: ".",
|
||||||
directory: Directory.Documents,
|
directory: Directory.Documents,
|
||||||
});
|
});
|
||||||
logger.log("[CapacitorPlatformService] Files in iOS Documents:", {
|
|
||||||
files: result.files.map((file) => (typeof file === "string" ? file : file.name)),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
const files = result.files.map((file) => ({
|
const files = result.files.map((file) => ({
|
||||||
name: typeof file === "string" ? file : file.name,
|
name: typeof file === "string" ? file : file.name,
|
||||||
uri: `file://${file.uri || file}`,
|
uri: `file://${file.uri || file}`,
|
||||||
@@ -1787,30 +1783,32 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
}));
|
}));
|
||||||
allFiles.push(...files);
|
allFiles.push(...files);
|
||||||
} else {
|
} else {
|
||||||
// Android: Search multiple locations where users might have saved files
|
// Android: Multiple locations with recursive search
|
||||||
|
|
||||||
// 1. App's default locations (for backward compatibility)
|
// 1. App's external storage directory
|
||||||
try {
|
|
||||||
const downloadsResult = await Filesystem.readdir({
|
|
||||||
path: "Download/TimeSafari",
|
|
||||||
directory: Directory.ExternalStorage,
|
|
||||||
});
|
|
||||||
const downloadFiles = downloadsResult.files.map((file) => ({
|
|
||||||
name: typeof file === "string" ? file : file.name,
|
|
||||||
uri: `file://${file.uri || file}`,
|
|
||||||
size: typeof file === "string" ? undefined : file.size,
|
|
||||||
path: "Download/TimeSafari"
|
|
||||||
}));
|
|
||||||
allFiles.push(...downloadFiles);
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const appStorageResult = await Filesystem.readdir({
|
const appStorageResult = await Filesystem.readdir({
|
||||||
path: "TimeSafari",
|
path: "TimeSafari",
|
||||||
directory: Directory.ExternalStorage,
|
directory: Directory.ExternalStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log full readdir output for TimeSafari
|
||||||
|
logger.log("[CapacitorPlatformService] Android TimeSafari readdir full result:", {
|
||||||
|
path: "TimeSafari",
|
||||||
|
directory: "ExternalStorage",
|
||||||
|
files: appStorageResult.files,
|
||||||
|
fileCount: appStorageResult.files.length,
|
||||||
|
fileDetails: appStorageResult.files.map((file, index) => ({
|
||||||
|
index,
|
||||||
|
name: typeof file === "string" ? file : file.name,
|
||||||
|
type: typeof file === "string" ? "string" : "object",
|
||||||
|
hasUri: typeof file === "string" ? false : !!file.uri,
|
||||||
|
hasSize: typeof file === "string" ? false : !!file.size,
|
||||||
|
fullObject: file
|
||||||
|
})),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
const appStorageFiles = appStorageResult.files.map((file) => ({
|
const appStorageFiles = appStorageResult.files.map((file) => ({
|
||||||
name: typeof file === "string" ? file : file.name,
|
name: typeof file === "string" ? file : file.name,
|
||||||
uri: `file://${file.uri || file}`,
|
uri: `file://${file.uri || file}`,
|
||||||
@@ -1822,7 +1820,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error);
|
logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Common user-chosen locations (if accessible)
|
// 2. Common user-chosen locations (if accessible) with recursive search
|
||||||
const commonPaths = [
|
const commonPaths = [
|
||||||
"Download",
|
"Download",
|
||||||
"Documents",
|
"Documents",
|
||||||
@@ -1830,7 +1828,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
"TimeSafari",
|
"TimeSafari",
|
||||||
"Data"
|
"Data"
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const path of commonPaths) {
|
for (const path of commonPaths) {
|
||||||
try {
|
try {
|
||||||
const result = await Filesystem.readdir({
|
const result = await Filesystem.readdir({
|
||||||
@@ -1838,22 +1836,94 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
directory: Directory.ExternalStorage,
|
directory: Directory.ExternalStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter for TimeSafari-related files
|
// Log full readdir output for debugging
|
||||||
const relevantFiles = result.files
|
logger.log(`[CapacitorPlatformService] Android ${path} readdir full result:`, {
|
||||||
.filter(file => {
|
path: path,
|
||||||
const fileName = typeof file === "string" ? file : file.name;
|
directory: "ExternalStorage",
|
||||||
const name = fileName.toLowerCase();
|
files: result.files,
|
||||||
return name.includes('timesafari') ||
|
fileCount: result.files.length,
|
||||||
|
fileDetails: result.files.map((file, index) => ({
|
||||||
|
index,
|
||||||
|
name: typeof file === "string" ? file : file.name,
|
||||||
|
type: typeof file === "string" ? "string" : "object",
|
||||||
|
hasUri: typeof file === "string" ? false : !!file.uri,
|
||||||
|
hasSize: typeof file === "string" ? false : !!file.size,
|
||||||
|
fullObject: file
|
||||||
|
})),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each entry (file or directory)
|
||||||
|
const relevantFiles = [];
|
||||||
|
for (const file of result.files) {
|
||||||
|
const fileName = typeof file === "string" ? file : file.name;
|
||||||
|
const name = fileName.toLowerCase();
|
||||||
|
|
||||||
|
// Check if it's a directory by trying to get file stats
|
||||||
|
let isDirectory = false;
|
||||||
|
try {
|
||||||
|
const stat = await Filesystem.stat({
|
||||||
|
path: `${path}/${fileName}`,
|
||||||
|
directory: Directory.ExternalStorage
|
||||||
|
});
|
||||||
|
isDirectory = stat.type === 'directory';
|
||||||
|
} catch (statError) {
|
||||||
|
// If stat fails, assume it's a file
|
||||||
|
isDirectory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDirectory) {
|
||||||
|
// RECURSIVELY SEARCH DIRECTORY for backup files
|
||||||
|
logger.log(`[CapacitorPlatformService] Recursively searching directory: ${fileName} in ${path}`);
|
||||||
|
try {
|
||||||
|
const subDirResult = await Filesystem.readdir({
|
||||||
|
path: `${path}/${fileName}`,
|
||||||
|
directory: Directory.ExternalStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process files in subdirectory
|
||||||
|
for (const subFile of subDirResult.files) {
|
||||||
|
const subFileName = typeof subFile === "string" ? subFile : subFile.name;
|
||||||
|
const subName = subFileName.toLowerCase();
|
||||||
|
|
||||||
|
// Check if subfile matches backup criteria
|
||||||
|
const matchesBackupCriteria = subName.includes('timesafari') ||
|
||||||
|
subName.includes('backup') ||
|
||||||
|
subName.includes('contacts') ||
|
||||||
|
subName.endsWith('.json');
|
||||||
|
|
||||||
|
if (matchesBackupCriteria) {
|
||||||
|
relevantFiles.push({
|
||||||
|
name: subFileName,
|
||||||
|
uri: `file://${subFile.uri || subFile}`,
|
||||||
|
size: typeof subFile === "string" ? undefined : subFile.size,
|
||||||
|
path: `${path}/${fileName}`
|
||||||
|
});
|
||||||
|
logger.log(`[CapacitorPlatformService] Found backup file in subdirectory: ${subFileName} in ${path}/${fileName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (subDirError) {
|
||||||
|
logger.warn(`[CapacitorPlatformService] Could not read subdirectory ${path}/${fileName}:`, subDirError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if file matches backup criteria
|
||||||
|
const matchesBackupCriteria = name.includes('timesafari') ||
|
||||||
name.includes('backup') ||
|
name.includes('backup') ||
|
||||||
name.includes('contacts') ||
|
name.includes('contacts') ||
|
||||||
name.endsWith('.json');
|
name.endsWith('.json');
|
||||||
})
|
|
||||||
.map((file) => ({
|
if (matchesBackupCriteria) {
|
||||||
name: typeof file === "string" ? file : file.name,
|
relevantFiles.push({
|
||||||
uri: `file://${file.uri || file}`,
|
name: fileName,
|
||||||
size: typeof file === "string" ? undefined : file.size,
|
uri: `file://${file.uri || file}`,
|
||||||
path: path
|
size: typeof file === "string" ? undefined : file.size,
|
||||||
}));
|
path: path
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log(`[CapacitorPlatformService] Excluding non-backup file: ${fileName} in ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (relevantFiles.length > 0) {
|
if (relevantFiles.length > 0) {
|
||||||
logger.log(`[CapacitorPlatformService] Found ${relevantFiles.length} relevant files in ${path}:`, {
|
logger.log(`[CapacitorPlatformService] Found ${relevantFiles.length} relevant files in ${path}:`, {
|
||||||
@@ -1889,57 +1959,325 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists files and folders in a given directory, with type detection.
|
* Test method to try different directory contexts and see what files are available
|
||||||
* Supports folder navigation for the backup browser UI.
|
* This helps debug file visibility issues across different storage contexts
|
||||||
* @param path - Directory path to list (relative to root or external storage)
|
* @returns Promise resolving to debug information about file discovery across contexts
|
||||||
* @param debugShowAll - If true, forcibly treat all entries as files (for debugging)
|
|
||||||
* @returns Promise resolving to array of file/folder info
|
|
||||||
*/
|
*/
|
||||||
async listFilesInDirectory(path: string = "Download/TimeSafari", debugShowAll: boolean = false): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> {
|
async testDirectoryContexts(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
logger.log('[DEBUG] Reading directory:', path);
|
let debugOutput = "=== Directory Context Testing ===\n\n";
|
||||||
const entries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = [];
|
|
||||||
const directory = this.getCapabilities().isIOS ? Directory.Documents : Directory.ExternalStorage;
|
if (this.getCapabilities().isIOS) {
|
||||||
const result = await Filesystem.readdir({ path, directory });
|
debugOutput += "iOS Platform - Testing Documents directory:\n";
|
||||||
logger.log('[DEBUG] Raw readdir result:', result.files);
|
try {
|
||||||
for (const entry of result.files) {
|
const result = await Filesystem.readdir({
|
||||||
const name = typeof entry === 'string' ? entry : entry.name;
|
path: ".",
|
||||||
const entryPath = path === '.' ? name : `${path}/${name}`;
|
directory: Directory.Documents,
|
||||||
let type: 'file' | 'folder' = 'file';
|
});
|
||||||
let size: number | undefined = undefined;
|
debugOutput += `Documents directory: ${result.files.length} files found\n`;
|
||||||
let uri: string = '';
|
result.files.forEach((file, index) => {
|
||||||
if (debugShowAll) {
|
const name = typeof file === "string" ? file : file.name;
|
||||||
// Forcibly treat all as files for debugging
|
debugOutput += ` ${index + 1}. ${name}\n`;
|
||||||
type = 'file';
|
});
|
||||||
uri = `file://${entryPath}`;
|
} catch (error) {
|
||||||
logger.log('[DEBUG] Forcing file type for entry:', { entryPath, type });
|
debugOutput += `Documents directory error: ${error}\n`;
|
||||||
} else {
|
}
|
||||||
try {
|
} else {
|
||||||
const stat = await Filesystem.stat({ path: entryPath, directory });
|
debugOutput += "Android Platform - Testing multiple directory contexts:\n\n";
|
||||||
if (stat.type === 'directory') {
|
|
||||||
type = 'folder';
|
const contexts = [
|
||||||
uri = '';
|
{ name: "Data (App Private)", directory: Directory.Data, path: "." },
|
||||||
} else {
|
{ name: "Documents", directory: Directory.Documents, path: "." },
|
||||||
type = 'file';
|
{ name: "External Storage Root", directory: Directory.ExternalStorage, path: "." },
|
||||||
size = stat.size;
|
{ name: "External Storage Downloads", directory: Directory.ExternalStorage, path: "Download" },
|
||||||
uri = stat.uri ? stat.uri : `file://${entryPath}`;
|
{ name: "External Storage TimeSafari", directory: Directory.ExternalStorage, path: "TimeSafari" },
|
||||||
}
|
{ name: "External Storage Download/TimeSafari", directory: Directory.ExternalStorage, path: "Download/TimeSafari" },
|
||||||
logger.log('[DEBUG] Stat for entry:', { entryPath, stat, type });
|
];
|
||||||
} catch (e) {
|
|
||||||
// If stat fails, assume file
|
for (const context of contexts) {
|
||||||
type = 'file';
|
debugOutput += `${context.name}:\n`;
|
||||||
uri = `file://${entryPath}`;
|
try {
|
||||||
logger.warn('[DEBUG] Stat failed for entry, assuming file:', { entryPath, error: e });
|
const result = await Filesystem.readdir({
|
||||||
}
|
path: context.path,
|
||||||
|
directory: context.directory,
|
||||||
|
});
|
||||||
|
debugOutput += ` Found ${result.files.length} entries\n`;
|
||||||
|
result.files.forEach((file, index) => {
|
||||||
|
const name = typeof file === "string" ? file : file.name;
|
||||||
|
debugOutput += ` ${index + 1}. ${name}\n`;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugOutput += ` Error: ${error}\n`;
|
||||||
|
}
|
||||||
|
debugOutput += "\n";
|
||||||
}
|
}
|
||||||
entries.push({ name, uri, size, path: entryPath, type });
|
|
||||||
}
|
}
|
||||||
// Sort: folders first, then files
|
|
||||||
entries.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'folder' ? -1 : 1));
|
debugOutput += "=== Context Testing Complete ===\n";
|
||||||
logger.log('[DEBUG] Final directoryEntries:', entries);
|
logger.log("[CapacitorPlatformService] Directory context test results:\n" + debugOutput);
|
||||||
|
return debugOutput;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `❌ Directory context testing failed: ${error}`;
|
||||||
|
logger.error("[CapacitorPlatformService] Directory context testing failed:", error);
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a test backup file to verify file writing and reading functionality.
|
||||||
|
* This is useful for debugging file visibility issues.
|
||||||
|
* @returns Promise resolving to success status and file information
|
||||||
|
*/
|
||||||
|
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> {
|
||||||
|
try {
|
||||||
|
logger.log("[CapacitorPlatformService] Creating test backup file for debugging");
|
||||||
|
|
||||||
|
// Create a simple test backup file
|
||||||
|
const testData = {
|
||||||
|
type: "test_backup",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
message: "This is a test backup file created for debugging file visibility issues",
|
||||||
|
contacts: [
|
||||||
|
{
|
||||||
|
did: "did:ethr:0x1234567890abcdef",
|
||||||
|
name: "Test Contact",
|
||||||
|
contactMethods: JSON.stringify([{ type: "EMAIL", value: "test@example.com" }]),
|
||||||
|
notes: "Test contact for debugging"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = JSON.stringify(testData, null, 2);
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const fileName = `TimeSafari-test-backup-${timestamp}.json`;
|
||||||
|
|
||||||
|
logger.log("[CapacitorPlatformService] Test backup file details:", {
|
||||||
|
fileName,
|
||||||
|
contentLength: content.length,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the test file using the same method as regular backups
|
||||||
|
const result = await this.writeAndShareFile(fileName, content, {
|
||||||
|
allowLocationSelection: false,
|
||||||
|
saveToDownloads: true,
|
||||||
|
showShareDialog: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.saved) {
|
||||||
|
logger.log("[CapacitorPlatformService] Test backup file created successfully:", {
|
||||||
|
fileName,
|
||||||
|
uri: result.uri,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
fileName,
|
||||||
|
uri: result.uri
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || "Failed to save test backup file");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
logger.error("[CapacitorPlatformService] Failed to create test backup file:", {
|
||||||
|
error: err.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to check what's actually in the TimeSafari directory
|
||||||
|
* This helps identify if the directory exists but is empty or has permission issues
|
||||||
|
* @returns Promise resolving to debug information about the TimeSafari directory
|
||||||
|
*/
|
||||||
|
async debugTimeSafariDirectory(): Promise<string> {
|
||||||
|
try {
|
||||||
|
let debugOutput = "=== TimeSafari Directory Debug ===\n\n";
|
||||||
|
|
||||||
|
if (this.getCapabilities().isIOS) {
|
||||||
|
debugOutput += "iOS: Checking Documents directory for TimeSafari files\n";
|
||||||
|
try {
|
||||||
|
const result = await Filesystem.readdir({
|
||||||
|
path: ".",
|
||||||
|
directory: Directory.Documents,
|
||||||
|
});
|
||||||
|
debugOutput += `Found ${result.files.length} files in Documents:\n`;
|
||||||
|
result.files.forEach((file, index) => {
|
||||||
|
const name = typeof file === "string" ? file : file.name;
|
||||||
|
debugOutput += ` ${index + 1}. ${name}\n`;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugOutput += `Error reading Documents: ${error}\n`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugOutput += "Android: Checking multiple TimeSafari locations\n\n";
|
||||||
|
|
||||||
|
// Test 1: Check if Download/TimeSafari exists and can be read
|
||||||
|
debugOutput += "1. Testing Download/TimeSafari directory:\n";
|
||||||
|
try {
|
||||||
|
const downloadResult = await Filesystem.readdir({
|
||||||
|
path: "Download/TimeSafari",
|
||||||
|
directory: Directory.ExternalStorage,
|
||||||
|
});
|
||||||
|
debugOutput += ` ✅ Success! Found ${downloadResult.files.length} files:\n`;
|
||||||
|
downloadResult.files.forEach((file, index) => {
|
||||||
|
const name = typeof file === "string" ? file : file.name;
|
||||||
|
const size = typeof file === "string" ? "unknown" : file.size;
|
||||||
|
debugOutput += ` ${index + 1}. ${name} (${size} bytes)\n`;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugOutput += ` ❌ Error: ${error}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Check if TimeSafari exists in root external storage
|
||||||
|
debugOutput += "\n2. Testing TimeSafari in root external storage:\n";
|
||||||
|
try {
|
||||||
|
const rootResult = await Filesystem.readdir({
|
||||||
|
path: "TimeSafari",
|
||||||
|
directory: Directory.ExternalStorage,
|
||||||
|
});
|
||||||
|
debugOutput += ` ✅ Success! Found ${rootResult.files.length} files:\n`;
|
||||||
|
rootResult.files.forEach((file, index) => {
|
||||||
|
const name = typeof file === "string" ? file : file.name;
|
||||||
|
const size = typeof file === "string" ? "unknown" : file.size;
|
||||||
|
debugOutput += ` ${index + 1}. ${name} (${size} bytes)\n`;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugOutput += ` ❌ Error: ${error}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Check what's in the Download directory
|
||||||
|
debugOutput += "\n3. Testing Download directory contents:\n";
|
||||||
|
try {
|
||||||
|
const downloadDirResult = await Filesystem.readdir({
|
||||||
|
path: "Download",
|
||||||
|
directory: Directory.ExternalStorage,
|
||||||
|
});
|
||||||
|
debugOutput += ` ✅ Success! Found ${downloadDirResult.files.length} items in Download:\n`;
|
||||||
|
downloadDirResult.files.forEach((file, index) => {
|
||||||
|
const name = typeof file === "string" ? file : file.name;
|
||||||
|
const size = typeof file === "string" ? "unknown" : file.size;
|
||||||
|
debugOutput += ` ${index + 1}. ${name} (${size} bytes)\n`;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugOutput += ` ❌ Error: ${error}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Try to create a test file in Download/TimeSafari
|
||||||
|
debugOutput += "\n4. Testing file creation in Download/TimeSafari:\n";
|
||||||
|
try {
|
||||||
|
const testResult = await this.createTestBackupFile();
|
||||||
|
if (testResult.success) {
|
||||||
|
debugOutput += ` ✅ Successfully created test file: ${testResult.fileName}\n`;
|
||||||
|
debugOutput += ` URI: ${testResult.uri}\n`;
|
||||||
|
} else {
|
||||||
|
debugOutput += ` ❌ Failed to create test file: ${testResult.error}\n`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugOutput += ` ❌ Error creating test file: ${error}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugOutput += "\n=== TimeSafari Directory Debug Complete ===\n";
|
||||||
|
logger.log("[CapacitorPlatformService] TimeSafari directory debug results:\n" + debugOutput);
|
||||||
|
return debugOutput;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `❌ TimeSafari directory debug failed: ${error}`;
|
||||||
|
logger.error("[CapacitorPlatformService] TimeSafari directory debug failed:", error);
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists files and folders in a specific directory for directory browsing
|
||||||
|
* @param path - The directory path to list
|
||||||
|
* @param debugShowAll - Debug flag to treat all entries as files
|
||||||
|
* @returns Promise resolving to array of directory entries
|
||||||
|
*/
|
||||||
|
async listFilesInDirectory(path: string, debugShowAll: boolean = false): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> {
|
||||||
|
try {
|
||||||
|
logger.log("[CapacitorPlatformService] Listing directory:", { path, debugShowAll });
|
||||||
|
|
||||||
|
let directory: Directory;
|
||||||
|
let actualPath: string;
|
||||||
|
|
||||||
|
if (this.getCapabilities().isIOS) {
|
||||||
|
directory = Directory.Documents;
|
||||||
|
actualPath = path === '.' ? '.' : path;
|
||||||
|
} else {
|
||||||
|
directory = Directory.ExternalStorage;
|
||||||
|
// Handle nested paths properly - use the full path as provided
|
||||||
|
actualPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("[CapacitorPlatformService] Attempting to read directory:", {
|
||||||
|
actualPath,
|
||||||
|
directory: directory.toString(),
|
||||||
|
platform: this.getCapabilities().isIOS ? "iOS" : "Android"
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Filesystem.readdir({
|
||||||
|
path: actualPath,
|
||||||
|
directory: directory,
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = [];
|
||||||
|
|
||||||
|
for (const file of result.files) {
|
||||||
|
const fileName = typeof file === "string" ? file : file.name;
|
||||||
|
|
||||||
|
// In debug mode, treat everything as a file
|
||||||
|
if (debugShowAll) {
|
||||||
|
entries.push({
|
||||||
|
name: fileName,
|
||||||
|
uri: `file://${file.uri || file}`,
|
||||||
|
size: typeof file === "string" ? undefined : file.size,
|
||||||
|
path: path,
|
||||||
|
type: 'file'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory by trying to get file stats
|
||||||
|
let isDirectory = false;
|
||||||
|
try {
|
||||||
|
const stat = await Filesystem.stat({
|
||||||
|
path: `${actualPath}/${fileName}`,
|
||||||
|
directory: directory
|
||||||
|
});
|
||||||
|
isDirectory = stat.type === 'directory';
|
||||||
|
} catch (statError) {
|
||||||
|
// If stat fails, assume it's a file
|
||||||
|
isDirectory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
name: fileName,
|
||||||
|
uri: `file://${file.uri || file}`,
|
||||||
|
size: typeof file === "string" ? undefined : file.size,
|
||||||
|
path: path,
|
||||||
|
type: isDirectory ? 'folder' : 'file'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("[CapacitorPlatformService] Directory listing result:", {
|
||||||
|
path,
|
||||||
|
entryCount: entries.length,
|
||||||
|
folders: entries.filter(e => e.type === 'folder').length,
|
||||||
|
files: entries.filter(e => e.type === 'file').length
|
||||||
|
});
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[CapacitorPlatformService] Failed to list files in directory:', { path, error });
|
logger.error("[CapacitorPlatformService] Failed to list directory:", { path, error });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists backup files specifically saved by the app.
|
* Lists backup files specifically saved by the app.
|
||||||
* Not implemented in Electron platform.
|
* Not implemented for Electron platform.
|
||||||
* @returns Promise resolving to empty array
|
* @returns Promise resolving to empty array
|
||||||
*/
|
*/
|
||||||
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
||||||
@@ -467,4 +467,43 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
|
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
|
||||||
return { success: false, error: "Directory access not implemented in Electron platform" };
|
return { success: false, error: "Directory access not implemented in Electron platform" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists files and folders in a specific directory for directory browsing.
|
||||||
|
* Not implemented for Electron platform.
|
||||||
|
* @returns Promise resolving to empty array
|
||||||
|
*/
|
||||||
|
async listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to check what's actually in the TimeSafari directory.
|
||||||
|
* Not implemented for Electron platform.
|
||||||
|
* @returns Promise resolving to debug information
|
||||||
|
*/
|
||||||
|
async debugTimeSafariDirectory(): Promise<string> {
|
||||||
|
return "Electron platform does not support file system access for debugging TimeSafari directory.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a test backup file to verify file writing and reading functionality.
|
||||||
|
* Not implemented for Electron platform.
|
||||||
|
* @returns Promise resolving to error status
|
||||||
|
*/
|
||||||
|
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Electron platform does not support file system access for creating test backup files."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to try different directory contexts and see what files are available.
|
||||||
|
* Not implemented for Electron platform.
|
||||||
|
* @returns Promise resolving to debug information
|
||||||
|
*/
|
||||||
|
async testDirectoryContexts(): Promise<string> {
|
||||||
|
return "Electron platform does not support file system access for testing directory contexts.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists backup files specifically saved by the app.
|
* Lists backup files specifically saved by the app.
|
||||||
* Not implemented in PyWebView platform.
|
* Not implemented for PyWebView platform.
|
||||||
* @returns Promise resolving to empty array
|
* @returns Promise resolving to empty array
|
||||||
*/
|
*/
|
||||||
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
||||||
@@ -246,4 +246,43 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
async rotateCamera(): Promise<void> {
|
async rotateCamera(): Promise<void> {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists files and folders in a specific directory for directory browsing.
|
||||||
|
* Not implemented for PyWebView platform.
|
||||||
|
* @returns Promise resolving to empty array
|
||||||
|
*/
|
||||||
|
async listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to check what's actually in the TimeSafari directory.
|
||||||
|
* Not implemented for PyWebView platform.
|
||||||
|
* @returns Promise resolving to debug information
|
||||||
|
*/
|
||||||
|
async debugTimeSafariDirectory(): Promise<string> {
|
||||||
|
return "PyWebView platform does not support file system access for debugging TimeSafari directory.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a test backup file to verify file writing and reading functionality.
|
||||||
|
* Not implemented for PyWebView platform.
|
||||||
|
* @returns Promise resolving to error status
|
||||||
|
*/
|
||||||
|
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "PyWebView platform does not support file system access for creating test backup files."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to try different directory contexts and see what files are available.
|
||||||
|
* Not implemented for PyWebView platform.
|
||||||
|
* @returns Promise resolving to debug information
|
||||||
|
*/
|
||||||
|
async testDirectoryContexts(): Promise<string> {
|
||||||
|
return "PyWebView platform does not support file system access for testing directory contexts.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,4 +513,43 @@ export class WebPlatformService implements PlatformService {
|
|||||||
// Not supported in web platform
|
// Not supported in web platform
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists files and folders in a specific directory for directory browsing.
|
||||||
|
* Not supported in web platform.
|
||||||
|
* @returns Promise resolving to empty array
|
||||||
|
*/
|
||||||
|
async listFilesInDirectory(path: string, debugShowAll?: boolean): Promise<Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}>> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug method to check what's actually in the TimeSafari directory.
|
||||||
|
* Not supported in web platform.
|
||||||
|
* @returns Promise resolving to debug information
|
||||||
|
*/
|
||||||
|
async debugTimeSafariDirectory(): Promise<string> {
|
||||||
|
return "Web platform does not support file system access for debugging TimeSafari directory.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a test backup file to verify file writing and reading functionality.
|
||||||
|
* Not supported in web platform.
|
||||||
|
* @returns Promise resolving to error status
|
||||||
|
*/
|
||||||
|
async createTestBackupFile(): Promise<{ success: boolean; fileName?: string; uri?: string; error?: string }> {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Web platform does not support file system access for creating test backup files."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to try different directory contexts and see what files are available.
|
||||||
|
* Not supported in web platform.
|
||||||
|
* @returns Promise resolving to debug information
|
||||||
|
*/
|
||||||
|
async testDirectoryContexts(): Promise<string> {
|
||||||
|
return "Web platform does not support file system access for testing directory contexts.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user