Skip to contents

Introduction

Imagine importing a set of photographs from a camera, smartphone, or scanner. Initially the files arrive as a flat collection with sequential filenames. During review, a person organises them into meaningful folders representing subjects, projects, or collections.

In this vignette we use nine tiny text files as stand-ins for photographs. The example demonstrates how fscontext records observations before and after this organisation, allowing the contextual changes to be reconstructed

library(fscontext, quietly = TRUE)
library(dplyr, quietly = TRUE)

vignette_folder <- file.path(tempdir(), "vignette")
camera <- file.path(vignette_folder, "camera")
photos <- file.path(vignette_folder, "photos")

unlink(c(vignette_folder, camera, photos), recursive = TRUE)

dir.create(vignette_folder)
dir.create(camera)
dir.create(photos)

for (i in 1:9) {
  writeLines(as.character(i),file.path(camera, paste0("P", i, ".txt")))
}
dir(camera)
#> [1] "P1.txt" "P2.txt" "P3.txt" "P4.txt" "P5.txt" "P6.txt" "P7.txt" "P8.txt"
#> [9] "P9.txt"

Let’s take a snapshot as the files arrive from a camera or stored on a memory card.

before <- snapshot_storage(path = camera, 
                           root = vignette_folder, 
                           person_id = "photographer")
#> Starting scan_storage() on: C:/Users/DANIEL~1/AppData/Local/Temp/RtmpWgD6tF/vignette
#> Scanning 9 files.
#> Signatures computed. Detecting repositories and Git status...
#> Files scanned: 9
#> Files in Git repos: 0
#> Files tracked by Git: 0
#> Skipped approximately 0 inaccessible files
#> scan_storage completed in 0.07 seconds
#> Saved: C:\Users\DANIEL~1\AppData\Local\Temp\RtmpWgD6tF/vignette/camera/scan_local-storage_c_vignette_20260715-110646_e5d247.rds

Summarise the files:

summary_before <- readRDS(before) |> 
  dplyr::select(person_id, rel_path, stem, size)

print(summary_before)
#>      person_id      rel_path stem size
#> 1 photographer camera/P1.txt   P1    3
#> 2 photographer camera/P2.txt   P2    3
#> 3 photographer camera/P3.txt   P3    3
#> 4 photographer camera/P4.txt   P4    3
#> 5 photographer camera/P5.txt   P5    3
#> 6 photographer camera/P6.txt   P6    3
#> 7 photographer camera/P7.txt   P7    3
#> 8 photographer camera/P8.txt   P8    3
#> 9 photographer camera/P9.txt   P9    3

Now let’s organise the photos into three groups: photos that are made inside of the house, in the garden, and photos that are not worth keeping and designated for deleting.

dir.create(file.path(photos, "house"), recursive = TRUE)
dir.create(file.path(photos, "garden"))
dir.create(file.path(photos, "delete"))

file.rename(
  file.path(camera, c("P1.txt","P2.txt","P5.txt")),
  file.path(photos, "house",
            c("P1.txt","P2.txt","P5.txt"))
)
#> [1] TRUE TRUE TRUE

file.rename(
  file.path(camera,
            c("P3.txt","P4.txt","P6.txt","P7.txt")),
  file.path(photos, "garden",
            c("P3.txt","P4.txt","P6.txt","P7.txt"))
)
#> [1] TRUE TRUE TRUE TRUE

file.rename(
  file.path(camera,
            c("P8.txt","P9.txt")),
  file.path(photos, "delete",
            c("P8.txt","P9.txt"))
)
#> [1] TRUE TRUE

The resulting file structure:

photos/

    house/
        P1.txt
        P2.txt
        P5.txt

    garden/
        P3.txt
        P4.txt
        P6.txt
        P7.txt

    delete/
        P8.txt
        P9.txt

The second snapshot records the reorganised collection. Although the files themselves have not changed, their contextual location within the filesystem has.

after <- snapshot_storage(path = photos, 
                          root = vignette_folder, 
                          person_id = "archivist")
#> Starting scan_storage() on: C:/Users/DANIEL~1/AppData/Local/Temp/RtmpWgD6tF/vignette
#> Scanning 10 files.
#> Signatures computed. Detecting repositories and Git status...
#> Files scanned: 10
#> Files in Git repos: 0
#> Files tracked by Git: 0
#> Skipped approximately 0 inaccessible files
#> scan_storage completed in 0.05 seconds
#> Saved: C:\Users\DANIEL~1\AppData\Local\Temp\RtmpWgD6tF/vignette/photos/scan_local-storage_c_vignette_20260715-110646_e5d247.rds

We can see the new summary:

summary_after <- readRDS(after) |> 
  dplyr::select(person_id, rel_path, stem, size)

print(summary_after)
#>    person_id                                                        rel_path
#> 1  archivist camera/scan_local-storage_c_vignette_20260715-110646_e5d247.rds
#> 2  archivist                                            photos/delete/P8.txt
#> 3  archivist                                            photos/delete/P9.txt
#> 4  archivist                                            photos/garden/P3.txt
#> 5  archivist                                            photos/garden/P4.txt
#> 6  archivist                                            photos/garden/P6.txt
#> 7  archivist                                            photos/garden/P7.txt
#> 8  archivist                                             photos/house/P1.txt
#> 9  archivist                                             photos/house/P2.txt
#> 10 archivist                                             photos/house/P5.txt
#>                                                    stem size
#> 1  scan_local-storage_c_vignette_20260715-110646_e5d247 1119
#> 2                                                    P8    3
#> 3                                                    P9    3
#> 4                                                    P3    3
#> 5                                                    P4    3
#> 6                                                    P6    3
#> 7                                                    P7    3
#> 8                                                    P1    3
#> 9                                                    P2    3
#> 10                                                   P5    3

Now compare the two observations.

The files are matched using their filename (stem) and file size. The comparison reveals how each file moved from the original camera import into its new contextual location.


summary_path_change <- summary_before |>
  rename(
    rel_path_before = rel_path,
    person_before = person_id
  ) |>
  left_join(
    summary_after |>
      rename(
        rel_path_after = rel_path,
        person_after = person_id
      ),
    by = c("stem", "size")
  ) |>
  select(
    stem, size,
    person_before, rel_path_before,
    rel_path_after, person_after
  ) |>
  filter(grepl("txt$", rel_path_before))


print(summary_path_change)
#>   stem size person_before rel_path_before       rel_path_after person_after
#> 1   P1    3  photographer   camera/P1.txt  photos/house/P1.txt    archivist
#> 2   P2    3  photographer   camera/P2.txt  photos/house/P2.txt    archivist
#> 3   P3    3  photographer   camera/P3.txt photos/garden/P3.txt    archivist
#> 4   P4    3  photographer   camera/P4.txt photos/garden/P4.txt    archivist
#> 5   P5    3  photographer   camera/P5.txt  photos/house/P5.txt    archivist
#> 6   P6    3  photographer   camera/P6.txt photos/garden/P6.txt    archivist
#> 7   P7    3  photographer   camera/P7.txt photos/garden/P7.txt    archivist
#> 8   P8    3  photographer   camera/P8.txt photos/delete/P8.txt    archivist
#> 9   P9    3  photographer   camera/P9.txt photos/delete/P9.txt    archivist

For example, photograph P1 was originally observed in the camera import and later observed in the house folder.

summary_path_change |>
  filter(stem == "P1") |>
  select(
    person_before,
    rel_path_before,
    rel_path_after,
    person_after
  ) |>
  tidyr::unite(
    col = "transition",
    rel_path_before,
    rel_path_after,
    sep = " → "
  )
#>   person_before                          transition person_after
#> 1  photographer camera/P1.txt → photos/house/P1.txt    archivist

The transition

camera/P1.txt → photos/house/P1.txt

records an observable change in filesystem context.

From the perspective of fscontext, this is evidence that the file became part of the contextual group house. The package deliberately does not infer why this happened. The folder may represent the subject of the photograph, a temporary workspace, or another organisational decision known only to the user.

The important point is that the filesystem itself records a human curation activity. By comparing two observational snapshots, fscontext reconstructs this activity as reproducible evidence.

The resulting comparison can serve as the starting point for later semantic review. A subsequent workflow may interpret the movement into house, garden, or delete as candidate semantic assertions, record the provenance of those assertions, and subject them to human review before they become part of a knowledge graph or archival description.