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" />
|
||||
Debug
|
||||
</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>
|
||||
|
||||
@@ -238,21 +252,57 @@ export default class BackupFilesList extends Vue {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
async mounted() {
|
||||
logger.log('[BackupFilesList] mounted hook called. platformCapabilities:', this.platformCapabilities);
|
||||
if (this.platformCapabilities.hasFileSystem) {
|
||||
// Set default root path
|
||||
if (this.platformCapabilities.isIOS) {
|
||||
this.currentPath = ['.'];
|
||||
} else {
|
||||
this.currentPath = ['Download', 'TimeSafari'];
|
||||
// Check/request permission before loading
|
||||
const hasPermission = await this.ensureStoragePermission();
|
||||
if (hasPermission) {
|
||||
// Set default root path
|
||||
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
|
||||
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
|
||||
*/
|
||||
get filteredFiles() {
|
||||
if (this.selectedType === 'all') {
|
||||
logger.log('[BackupFilesList] filteredFiles (All):', 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
|
||||
*/
|
||||
async refreshFiles() {
|
||||
logger.log('[BackupFilesList] refreshFiles called.');
|
||||
if (!this.platformCapabilities.hasFileSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check/request permission before refreshing
|
||||
const hasPermission = await this.ensureStoragePermission();
|
||||
if (!hasPermission) {
|
||||
this.backupFiles = [];
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
try {
|
||||
this.backupFiles = await this.platformService.listBackupFiles();
|
||||
|
||||
logger.log("[BackupFilesList] Refreshed backup files:", {
|
||||
logger.log('[BackupFilesList] Refreshed backup files:', {
|
||||
count: this.backupFiles.length,
|
||||
files: this.backupFiles.map(f => ({
|
||||
name: f.name,
|
||||
@@ -367,7 +427,6 @@ export default class BackupFilesList extends Vue {
|
||||
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Debug: Log file type distribution
|
||||
const typeCounts = {
|
||||
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,
|
||||
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) {
|
||||
logger.error("[BackupFilesList] Failed to refresh backup files:", error);
|
||||
logger.error('[BackupFilesList] Failed to refresh backup files:', error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -393,11 +454,103 @@ export default class BackupFilesList extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to refresh files from external components
|
||||
* Used by DataExportSection to refresh after saving new files
|
||||
* Creates a test backup file for debugging purposes
|
||||
*/
|
||||
public async refreshAfterSave() {
|
||||
logger.log("[BackupFilesList] Refreshing files after save operation");
|
||||
async createTestBackup() {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -462,14 +615,18 @@ export default class BackupFilesList extends Vue {
|
||||
|
||||
/**
|
||||
* Gets the count of files for a specific type
|
||||
* @param type - File type to count
|
||||
* @returns Number of files of the specified type
|
||||
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
|
||||
*/
|
||||
getFileCountByType(type: 'all' | 'contacts' | 'seed' | 'other'): number {
|
||||
let count;
|
||||
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
|
||||
*/
|
||||
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}> = [];
|
||||
|
||||
if (this.getCapabilities().isIOS) {
|
||||
// iOS: List files in Documents directory
|
||||
// iOS: Documents directory
|
||||
const result = await Filesystem.readdir({
|
||||
path: ".",
|
||||
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) => ({
|
||||
name: typeof file === "string" ? file : file.name,
|
||||
uri: `file://${file.uri || file}`,
|
||||
@@ -1787,30 +1783,32 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
}));
|
||||
allFiles.push(...files);
|
||||
} 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)
|
||||
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);
|
||||
}
|
||||
|
||||
// 1. App's external storage directory
|
||||
try {
|
||||
const appStorageResult = await Filesystem.readdir({
|
||||
path: "TimeSafari",
|
||||
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) => ({
|
||||
name: typeof file === "string" ? file : file.name,
|
||||
uri: `file://${file.uri || file}`,
|
||||
@@ -1822,7 +1820,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
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 = [
|
||||
"Download",
|
||||
"Documents",
|
||||
@@ -1830,7 +1828,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
"TimeSafari",
|
||||
"Data"
|
||||
];
|
||||
|
||||
|
||||
for (const path of commonPaths) {
|
||||
try {
|
||||
const result = await Filesystem.readdir({
|
||||
@@ -1838,22 +1836,94 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
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') ||
|
||||
// Log full readdir output for debugging
|
||||
logger.log(`[CapacitorPlatformService] Android ${path} readdir full result:`, {
|
||||
path: path,
|
||||
directory: "ExternalStorage",
|
||||
files: result.files,
|
||||
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('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
|
||||
}));
|
||||
|
||||
if (matchesBackupCriteria) {
|
||||
relevantFiles.push({
|
||||
name: fileName,
|
||||
uri: `file://${file.uri || file}`,
|
||||
size: typeof file === "string" ? undefined : file.size,
|
||||
path: path
|
||||
});
|
||||
} else {
|
||||
logger.log(`[CapacitorPlatformService] Excluding non-backup file: ${fileName} in ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (relevantFiles.length > 0) {
|
||||
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.
|
||||
* Supports folder navigation for the backup browser UI.
|
||||
* @param path - Directory path to list (relative to root or external storage)
|
||||
* @param debugShowAll - If true, forcibly treat all entries as files (for debugging)
|
||||
* @returns Promise resolving to array of file/folder info
|
||||
* Test method to try different directory contexts and 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
|
||||
*/
|
||||
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 {
|
||||
logger.log('[DEBUG] Reading directory:', path);
|
||||
const entries: Array<{name: string, uri: string, size?: number, path: string, type: 'file' | 'folder'}> = [];
|
||||
const directory = this.getCapabilities().isIOS ? Directory.Documents : Directory.ExternalStorage;
|
||||
const result = await Filesystem.readdir({ path, directory });
|
||||
logger.log('[DEBUG] Raw readdir result:', result.files);
|
||||
for (const entry of result.files) {
|
||||
const name = typeof entry === 'string' ? entry : entry.name;
|
||||
const entryPath = path === '.' ? name : `${path}/${name}`;
|
||||
let type: 'file' | 'folder' = 'file';
|
||||
let size: number | undefined = undefined;
|
||||
let uri: string = '';
|
||||
if (debugShowAll) {
|
||||
// Forcibly treat all as files for debugging
|
||||
type = 'file';
|
||||
uri = `file://${entryPath}`;
|
||||
logger.log('[DEBUG] Forcing file type for entry:', { entryPath, type });
|
||||
} else {
|
||||
try {
|
||||
const stat = await Filesystem.stat({ path: entryPath, directory });
|
||||
if (stat.type === 'directory') {
|
||||
type = 'folder';
|
||||
uri = '';
|
||||
} else {
|
||||
type = 'file';
|
||||
size = stat.size;
|
||||
uri = stat.uri ? stat.uri : `file://${entryPath}`;
|
||||
}
|
||||
logger.log('[DEBUG] Stat for entry:', { entryPath, stat, type });
|
||||
} catch (e) {
|
||||
// If stat fails, assume file
|
||||
type = 'file';
|
||||
uri = `file://${entryPath}`;
|
||||
logger.warn('[DEBUG] Stat failed for entry, assuming file:', { entryPath, error: e });
|
||||
}
|
||||
let debugOutput = "=== Directory Context Testing ===\n\n";
|
||||
|
||||
if (this.getCapabilities().isIOS) {
|
||||
debugOutput += "iOS Platform - Testing Documents directory:\n";
|
||||
try {
|
||||
const result = await Filesystem.readdir({
|
||||
path: ".",
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
debugOutput += `Documents directory: ${result.files.length} files found\n`;
|
||||
result.files.forEach((file, index) => {
|
||||
const name = typeof file === "string" ? file : file.name;
|
||||
debugOutput += ` ${index + 1}. ${name}\n`;
|
||||
});
|
||||
} catch (error) {
|
||||
debugOutput += `Documents directory error: ${error}\n`;
|
||||
}
|
||||
} else {
|
||||
debugOutput += "Android Platform - Testing multiple directory contexts:\n\n";
|
||||
|
||||
const contexts = [
|
||||
{ name: "Data (App Private)", directory: Directory.Data, path: "." },
|
||||
{ name: "Documents", directory: Directory.Documents, path: "." },
|
||||
{ name: "External Storage Root", directory: Directory.ExternalStorage, path: "." },
|
||||
{ name: "External Storage Downloads", directory: Directory.ExternalStorage, path: "Download" },
|
||||
{ name: "External Storage TimeSafari", directory: Directory.ExternalStorage, path: "TimeSafari" },
|
||||
{ name: "External Storage Download/TimeSafari", directory: Directory.ExternalStorage, path: "Download/TimeSafari" },
|
||||
];
|
||||
|
||||
for (const context of contexts) {
|
||||
debugOutput += `${context.name}:\n`;
|
||||
try {
|
||||
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));
|
||||
logger.log('[DEBUG] Final directoryEntries:', entries);
|
||||
|
||||
debugOutput += "=== Context Testing Complete ===\n";
|
||||
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;
|
||||
} catch (error) {
|
||||
logger.error('[CapacitorPlatformService] Failed to list files in directory:', { path, error });
|
||||
logger.error("[CapacitorPlatformService] Failed to list directory:", { path, error });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ export class ElectronPlatformService implements PlatformService {
|
||||
|
||||
/**
|
||||
* Lists backup files specifically saved by the app.
|
||||
* Not implemented in Electron platform.
|
||||
* Not implemented for Electron platform.
|
||||
* @returns Promise resolving to empty array
|
||||
*/
|
||||
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 }> {
|
||||
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.
|
||||
* Not implemented in PyWebView platform.
|
||||
* Not implemented for PyWebView platform.
|
||||
* @returns Promise resolving to empty array
|
||||
*/
|
||||
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> {
|
||||
// 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
|
||||
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