Read and Write Directories
Understanding the directory mechanics in processors
Read and Write Directories
Processors work with two directories: readDir for input and writeDir for output. Understanding these is key to correct processor implementation.
Directory Overview
| Directory | Path | Purpose | Access |
|---|---|---|---|
readDir | /workspace/cyanprint/ | Source files | Read-only |
writeDir | /workspace/output/ | Generated files | Write-only |
The paths shown above are typical examples. Actual values depend on your container or deployment configuration.
readDir
Contains files from the template's blob image. This is your source material.
Typical Value
/workspace/cyanprint/
Contents
Files from the template that match the processor's globs:
/workspace/cyanprint/├── templates/│ ├── README.md│ ├── package.json│ └── src/│ └── index.ts└── config/└── settings.json
Accessing Files
Use CyanFileHelper - never access directly:
StartProcessorWithLambda(async (input, fileHelper) => {// ✅ Correct: Use fileHelperconst files = fileHelper.resolveAll();// ❌ Wrong: Direct filesystem access// const content = fs.readFileSync(`${input.readDir}/README.md`);return { directory: input.writeDir };});
def start_processor_with_fn(input, file_helper):# ✅ Correct: Use file_helperfiles = file_helper.resolve_all()# ❌ Wrong: Direct filesystem access# content = open(f'{input.read_dir}/README.md').read()return {'directory': input.write_dir}
[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){// ✅ Correct: Use fileHelpervar files = fileHelper.ResolveAll();// ❌ Wrong: Direct filesystem access// var content = File.ReadAllText($"{input.ReadDir}/README.md");return new ProcessorResult { Directory = input.WriteDir };}
Never access readDir directly with fs operations. Always use CyanFileHelper APIs.
writeDir
Where processed files are written. This is your output location.
Typical Value
/workspace/output/
Writing Files
Use writeFile() on file objects:
StartProcessorWithLambda(async (input, fileHelper) => {const files = fileHelper.resolveAll();files.forEach(file => {// Transform contentfile.content = transform(file.content);// Write to writeDirfile.writeFile();});return { directory: input.writeDir };});
def start_processor_with_fn(input, file_helper):files = file_helper.resolve_all()for file in files:# Transform contentfile.content = transform(file.content)# Write to write_dirfile.write_file()return {'directory': input.write_dir}
[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){var files = fileHelper.ResolveAll();foreach (var file in files){// Transform contentfile.Content = Transform(file.Content);// Write to WriteDirfile.WriteFile();}return new ProcessorResult { Directory = input.WriteDir };}
VirtualFile Paths
The relative property is the path between read and write:
// Input: /workspace/cyanprint/templates/README.md// file.relative = 'templates/README.md'// Output: /workspace/output/templates/README.md
# Input: /workspace/cyanprint/templates/README.md# file.relative = 'templates/README.md'# Output: /workspace/output/templates/README.md
// Input: /workspace/cyanprint/templates/README.md// file.Relative = "templates/README.md"// Output: /workspace/output/templates/README.md
Path Resolution
How Paths Work
// Full input pathconst inputPath = `${input.readDir}/${file.relative}`;// Example: /workspace/cyanprint/templates/README.md// Full output pathconst outputPath = `${input.writeDir}/${file.relative}`;// Example: /workspace/output/templates/README.md
# Full input pathinput_path = f'{input.read_dir}/{file.relative}'# Example: /workspace/cyanprint/templates/README.md# Full output pathoutput_path = f'{input.write_dir}/{file.relative}'# Example: /workspace/output/templates/README.md
// Full input pathvar inputPath = $"{input.ReadDir}/{file.Relative}";// Example: /workspace/cyanprint/templates/README.md// Full output pathvar outputPath = $"{input.WriteDir}/{file.Relative}";// Example: /workspace/output/templates/README.md
Modifying Output Paths
You can change where files are written:
StartProcessorWithLambda(async (input, fileHelper) => {const files = fileHelper.resolveAll();files.forEach(file => {// Transform contentfile.content = transform(file.content);// Optionally change output pathif (file.relative.endsWith('.template')) {// Remove .template extensionfile.relative = file.relative.replace('.template', '');}file.writeFile();});return { directory: input.writeDir };});
def start_processor_with_fn(input, file_helper):files = file_helper.resolve_all()for file in files:# Transform contentfile.content = transform(file.content)# Optionally change output pathif file.relative.endswith('.template'):# Remove .template extensionfile.relative = file.relative.replace('.template', '')file.write_file()return {'directory': input.write_dir}
[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){var files = fileHelper.ResolveAll();foreach (var file in files){// Transform contentfile.Content = Transform(file.Content);// Optionally change output pathif (file.Relative.EndsWith(".template")){// Remove .template extensionfile.Relative = file.Relative.Replace(".template", "");}file.WriteFile();}return new ProcessorResult { Directory = input.WriteDir };}
Important Rules
Rule 1: Always Return writeDir
// ✅ Correctreturn { directory: input.writeDir };// ❌ Wrong - never change this// return { directory: '/some/other/path' };
# ✅ Correctreturn {'directory': input.write_dir}# ❌ Wrong - never change this# return {'directory': '/some/other/path'}
// ✅ Correctreturn new ProcessorResult { Directory = input.WriteDir };// ❌ Wrong - never change this// return new ProcessorResult { Directory = "/some/other/path" };
Rule 2: Use fileHelper for All File Operations
// ✅ Correctconst files = fileHelper.resolveAll();files.forEach(f => f.writeFile());// ❌ Wrong// fs.readdirSync(input.readDir);// fs.writeFileSync(`${input.writeDir}/file.txt`, content);
# ✅ Correctfiles = file_helper.resolve_all()for f in files:f.write_file()# ❌ Wrong# os.listdir(input.read_dir)# open(f'{input.write_dir}/file.txt', 'w').write(content)
// ✅ Correctvar files = fileHelper.ResolveAll();foreach (var f in files) f.WriteFile();// ❌ Wrong// Directory.GetFiles(input.ReadDir);// File.WriteAllText($"{input.WriteDir}/file.txt", content);
Rule 3: Preserve Directory Structure (Usually)
// Usually you want to maintain the same structurefiles.forEach(file => {// file.relative is preservedfile.writeFile();});// Output mirrors input structure
# Usually you want to maintain the same structurefor file in files:# file.relative is preservedfile.write_file()# Output mirrors input structure
// Usually you want to maintain the same structureforeach (var file in files){// file.Relative is preservedfile.WriteFile();}// Output mirrors input structure
Rule 4: Handle Missing Writes
Files not written are not included in output:
Note: Copy-type files (matched by copy globs in cyan.yaml) are automatically written during resolveAll(). Template-type files require explicit writeFile() calls. If you don't call writeFile() on a template file, it won't be included in the output.
files.forEach(file => {if (shouldInclude(file)) {file.writeFile(); // Included}// Template files without writeFile() are excluded// Copy-type files are already handled by resolveAll()});
for file in files:if should_include(file):file.write_file() # Included# Template files without write_file() are excluded# Copy-type files are already handled by resolve_all()
foreach (var file in files){if (ShouldInclude(file)){file.WriteFile(); // Included}// Template files without WriteFile() are excluded// Copy-type files are already handled by ResolveAll()}
Example: Full Path Flow
StartProcessorWithLambda(async (input, fileHelper) => {console.log('Reading from:', input.readDir);// /workspace/cyanprint/console.log('Writing to:', input.writeDir);// /workspace/output/// resolveAll() copies Copy-type files automatically, returns Template-type filesconst files = fileHelper.resolveAll();files.forEach(file => {console.log('Processing:', file.relative);// templates/README.md// Full paths (conceptual - don't access directly)// Input: /workspace/cyanprint/templates/README.md// Output: /workspace/output/templates/README.mdfile.content = transform(file.content);file.writeFile();});return { directory: input.writeDir };});
def start_processor_with_fn(input, file_helper):print('Reading from:', input.read_dir)# /workspace/cyanprint/print('Writing to:', input.write_dir)# /workspace/output/# resolve_all() copies Copy-type files automatically, returns Template-type filesfiles = file_helper.resolve_all()for file in files:print('Processing:', file.relative)# templates/README.md# Full paths (conceptual - don't access directly)# Input: /workspace/cyanprint/templates/README.md# Output: /workspace/output/templates/README.mdfile.content = transform(file.content)file.write_file()return {'directory': input.write_dir}
[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){Console.WriteLine($"Reading from: {input.ReadDir}");// /workspace/cyanprint/Console.WriteLine($"Writing to: {input.WriteDir}");// /workspace/output/// ResolveAll() copies Copy-type files automatically, returns Template-type filesvar files = fileHelper.ResolveAll();foreach (var file in files){Console.WriteLine($"Processing: {file.Relative}");// templates/README.md// Full paths (conceptual - don't access directly)// Input: /workspace/cyanprint/templates/README.md// Output: /workspace/output/templates/README.mdfile.Content = Transform(file.Content);file.WriteFile();}return new ProcessorResult { Directory = input.WriteDir };}
Related
- CyanFileHelper API - File operations
- Input/Output Types - Type definitions
- Stateless Nature - Why isolation matters