Tutorial

FILE_FOLDER_FILTERING_GUIDE

File & Folder Filtering Guide with Sharing Support

Overview

This guide explains how to properly use the FileSystemCache and EnhancedFileSystemCache APIs for file and folder filtering with the new sharing structure. The cache supports both room owners (who can see all their files) and non-owners (who see room-specific files).

Table of Contents

  1. Cache Initialization
  2. Room Owner Filtering
  3. Understanding File Inclusion Rules
  4. Complete Implementation Examples
  5. API Reference
  6. Common Pitfalls

Cache Initialization

Setting Up the Cache

import { getFileSystemCache } from './api/FileSystemCache.js';
import { EnhancedFileSystemCache } from './api/EnhancedFileSystemCache.js';

// Get cache instance
const cache = getFileSystemCache({
  client: apiClient,
  userId: currentUser.id,
  roomName: currentRoom,
  isRoomOwner: isCurrentUserRoomOwner  // CRITICAL: Must be set correctly!
});

Important: The isRoomOwner parameter determines:

  • Room Owners: Get one cache with ALL files across ALL rooms
  • Non-Owners: Get room-specific cache with filtered files

Room Owner Filtering

Loading Filtered Content

Room owners can toggle between viewing all their files or just files for the current room:

async function loadFolderContents(folderPath) {
  const isRoomOwner = true; // Determined from your auth/room logic
  const showRoomFilesOnly = getUserPreference('showRoomFilesOnly');

  if (isRoomOwner && showRoomFilesOnly) {
    // Option A: Get filtered tree (recommended - includes shared files)
    const filteredTree = await EnhancedFileSystemCache.getFilteredFolderTreeWithInit(
      cache,
      currentRoom
    );

    // Option B: Or use the cache method directly
    const roomSpecificTree = await cache.getFileTreeByRoom(currentRoom);

    // Extract folder contents from filtered tree
    const folderNode = extractFolderFromTree(filteredTree, folderPath);
    return folderNode;

  } else {
    // Show all files (no filtering)
    const fullTree = await EnhancedFileSystemCache.getFileTreeWithInit(cache);
    const folderNode = extractFolderFromTree(fullTree, folderPath);
    return folderNode;
  }
}

Understanding File Inclusion Rules

What Gets Included When Filtering by Room

When filtering is applied, files are included based on these rules:

function isFileIncludedInRoomFilter(file, targetRoom, currentUserId) {
  // 1. Files that belong to the room directly
  if (file.roomName === targetRoom) {
    return true;
  }

  // 2. Files shared WITH the current user IN this room
  if (file.sharing && Array.isArray(file.sharing)) {
    return file.sharing.some(share =>
      share.userId === currentUserId &&
      share.roomName === targetRoom
    );
  }

  return false;
}

Sharing Structure

The new sharing structure provides granular control:

// File sharing structure
{
  sharing: [{
    userId: "user123",
    roomName: "popsyroom1",
    permissions: ["read", "write"]
  }]
}

Complete Implementation Examples

Vanilla JavaScript FileManager

class FileManager {
  constructor(apiClient, currentUser, currentRoom, isRoomOwner) {
    this.cache = getFileSystemCache({
      client: apiClient,
      userId: currentUser.id,
      roomName: currentRoom,
      isRoomOwner: isRoomOwner  // MUST be set!
    });

    this.currentRoom = currentRoom;
    this.currentUser = currentUser;
    this.isRoomOwner = isRoomOwner;
    this.showRoomFilesOnly = false; // Toggle state
  }

  async loadFolder(folderPath = '/') {
    try {
      let tree;

      // Room owners can filter by room
      if (this.isRoomOwner && this.showRoomFilesOnly) {
        // Use filtered tree that includes shared files
        tree = await EnhancedFileSystemCache.getFilteredFolderTreeWithInit(
          this.cache,
          this.currentRoom
        );
        console.log('📁 Using filtered tree for room:', this.currentRoom);
      } else {
        // Use full tree
        tree = await EnhancedFileSystemCache.getFileTreeWithInit(this.cache);
        console.log('📁 Using full tree (no filtering)');
      }

      // Navigate to the folder in the tree
      const folder = this.navigateToPath(tree, folderPath);

      // Get files and folders
      const contents = {
        files: [],
        folders: []
      };

      if (folder && folder.children) {
        Object.values(folder.children).forEach(child => {
          if (child.type === 'folder') {
            contents.folders.push(child);
          } else {
            contents.files.push(child);
          }
        });
      }

      return contents;
    } catch (error) {
      console.error('Error loading folder:', error);
      throw error;
    }
  }

  // Helper to navigate tree to a specific path
  navigateToPath(tree, targetPath) {
    if (!targetPath || targetPath === '/') {
      return tree;
    }

    const segments = targetPath.split('/').filter(Boolean);
    let current = tree;

    for (const segment of segments) {
      if (!current.children) return null;

      current = Object.values(current.children).find(child =>
        child.name === segment && child.type === 'folder'
      );

      if (!current) return null;
    }

    return current;
  }

  // Check if a file is shared in the current room
  isFileShared(file) {
    if (!file.sharing || !Array.isArray(file.sharing)) {
      return false;
    }

    // Check if file is shared with anyone in current room
    return file.sharing.some(share =>
      share.roomName === this.currentRoom
    );
  }

  // Check if file is shared with current user
  isFileSharedWithMe(file) {
    if (!file.sharing || !Array.isArray(file.sharing)) {
      return false;
    }

    return file.sharing.some(share =>
      share.userId === this.currentUser.id &&
      share.roomName === this.currentRoom
    );
  }

  // Toggle room filtering
  toggleRoomFiltering() {
    this.showRoomFilesOnly = !this.showRoomFilesOnly;
    // Reload current folder with new filtering
    this.loadFolder(this.currentPath);
  }
}

React Component Example

function FileExplorer({ apiClient, currentUser, currentRoom, isRoomOwner }) {
  const [showRoomFilesOnly, setShowRoomFilesOnly] = useState(false);
  const [folderContents, setFolderContents] = useState({ files: [], folders: [] });
  const [currentPath, setCurrentPath] = useState('/');

  // Initialize cache with proper room ownership
  const cache = useMemo(() => {
    return getFileSystemCache({
      client: apiClient,
      userId: currentUser.id,
      roomName: currentRoom,
      isRoomOwner: isRoomOwner  // Critical!
    });
  }, [apiClient, currentUser.id, currentRoom, isRoomOwner]);

  // Load folder contents
  const loadFolder = useCallback(async (path) => {
    try {
      let tree;

      if (isRoomOwner && showRoomFilesOnly) {
        // Filtered tree - includes owned files + shared files in this room
        tree = await EnhancedFileSystemCache.getFilteredFolderTreeWithInit(
          cache,
          currentRoom
        );
      } else {
        // Full tree - all files
        tree = await EnhancedFileSystemCache.getFileTreeWithInit(cache);
      }

      // Extract folder at path
      const folder = navigateToPath(tree, path);

      // Convert to files/folders arrays
      const contents = { files: [], folders: [] };
      if (folder?.children) {
        Object.values(folder.children).forEach(child => {
          if (child.type === 'folder') {
            contents.folders.push(child);
          } else {
            // Add sharing indicator
            contents.files.push({
              ...child,
              isShared: child.sharing?.length > 0,
              sharedWithMe: child.sharing?.some(s =>
                s.userId === currentUser.id &&
                s.roomName === currentRoom
              )
            });
          }
        });
      }

      setFolderContents(contents);
    } catch (error) {
      console.error('Failed to load folder:', error);
    }
  }, [cache, currentRoom, showRoomFilesOnly, isRoomOwner, currentUser.id]);

  // Reload when filter changes
  useEffect(() => {
    loadFolder(currentPath);
  }, [currentPath, showRoomFilesOnly, loadFolder]);

  return (
    <div>
      {isRoomOwner && (
        <button onClick={() => setShowRoomFilesOnly(!showRoomFilesOnly)}>
          {showRoomFilesOnly ? 'Show All Files' : 'Show Room Files Only'}
        </button>
      )}

      <div className="folders">
        {folderContents.folders.map(folder => (
          <FolderItem
            key={folder.id}
            folder={folder}
            onClick={() => setCurrentPath(folder.path)}
          />
        ))}
      </div>

      <div className="files">
        {folderContents.files.map(file => (
          <FileItem
            key={file.id}
            file={file}
            isShared={file.isShared}
            sharedWithMe={file.sharedWithMe}
          />
        ))}
      </div>
    </div>
  );
}

// Helper function for navigating tree
function navigateToPath(tree, targetPath) {
  if (!targetPath || targetPath === '/') {
    return tree;
  }

  const segments = targetPath.split('/').filter(Boolean);
  let current = tree;

  for (const segment of segments) {
    if (!current.children) return null;

    current = Object.values(current.children).find(child =>
      child.name === segment && child.type === 'folder'
    );

    if (!current) return null;
  }

  return current;
}

API Reference

Available Methods

Method Returns Use Case
getFileTree() All files in cache Show everything (no filtering)
getFileTreeByRoom(room) Files owned in room + files shared in room Room filtering for owners
getFilteredFolderTreeWithInit(cache, room) Recursive filtered tree Deep folder filtering
getFilesByRoom(room) Flat array of room files Getting file list only
folderContainsRoomFilesWithInit(cache, path, room) Boolean Check if folder has room files

Cache Methods for Room Owners

// Get all files (no filtering)
const allFiles = await cache.getAllFiles();

// Get files for specific room (includes shared files)
const roomFiles = await cache.getFilesByRoom('popsyroom1');

// Get filtered tree for room
const roomTree = await cache.getFileTreeByRoom('popsyroom1');

// Check if folder contains files from room
const hasRoomFiles = await EnhancedFileSystemCache.folderContainsRoomFilesWithInit(
  cache,
  '/Whiteboards',
  'popsyroom1'
);

Cache Methods for Non-Owners

// Non-owners always get room-specific data
const files = await cache.getAllFiles(); // Returns only room files
const tree = await cache.getFileTree();  // Returns only room tree

Common Pitfalls

❌ Don't Do This

// WRONG - This doesn't filter folders properly
const tree = await cache.getFileTree();
const filtered = myCustomFilter(tree); // Don't do client-side filtering

// WRONG - Using old sharedWith field (removed)
const isShared = file.sharedWith?.includes(currentUser.id);

// WRONG - Not setting isRoomOwner
const cache = getFileSystemCache({
  client: apiClient,
  userId: currentUser.id,
  roomName: currentRoom
  // Missing isRoomOwner!
});

// WRONG - Not checking sharing structure properly
if (file.sharing) { // Need to check array and room
  return true;
}

✅ Do This Instead

// CORRECT - Use proper filtering methods
if (isRoomOwner && showRoomFilesOnly) {
  const tree = await cache.getFileTreeByRoom(currentRoom);
}

// CORRECT - Check sharing properly
const isSharedInThisRoom = file.sharing?.some(share =>
  share.userId === currentUser.id &&
  share.roomName === currentRoom
);

// CORRECT - Always set isRoomOwner
const cache = getFileSystemCache({
  client: apiClient,
  userId: currentUser.id,
  roomName: currentRoom,
  isRoomOwner: isCurrentUserRoomOwner // Required!
});

// CORRECT - Check both user and room in sharing
const hasAccess = file.sharing?.some(share =>
  share.userId === currentUser.id &&
  share.roomName === currentRoom
);

Key Concepts

Database Structure

  • Room Owners: One database named muzie-file-system-cache-${userId}
  • Non-Owners: Room-specific database named muzie-file-system-cache-${userId}::${roomName}

Filtering Logic

The filtering works recursively:

  1. Check if file/folder belongs to the target room
  2. Check if file is shared with the current user in the target room
  3. Include folders only if they contain relevant files (at any depth)

Sharing Permissions

The sharing structure supports permissions for future features:

{
  userId: "user123",
  roomName: "room1",
  permissions: ["read", "write", "delete"]
}

Currently, all shares default to ["read"] permissions.

Migration Notes

If migrating from the old system:

  1. Remove references to sharedWith - This field has been completely removed
  2. Update to use sharing array - The new structure with userId, roomName, permissions
  3. Set isRoomOwner correctly - Critical for proper cache behavior
  4. Use new filtering methods - Don't implement custom filtering in the UI

Support

For questions or issues with the file system cache, please check:

  • The cache implementation in /src/api/FileSystemCache.js
  • The enhanced cache wrapper in /src/api/EnhancedFileSystemCache.js
  • The test suite in /src/api/__tests__/FileSystemCache.test.js