File Handling and I/O
Practice and Assessment
You are viewing a free preview lesson.
Project Overview
এই module-এর final project-এ আমরা একটি small file-based course storage system তৈরি করব।
Systemটি support করবে:
- Course save
- Course load
- Course list
- Course delete
- UTF-8 text storage
- Safe path handling
- Atomic replacement
- Repository abstraction
- Missing course handling
- Storage exception translation
- Backup creation
এই project-এর goal production-ready database বানানো নয়।
Goal হলো Module 6-এর core I/O concepts একসঙ্গে apply করা।
Project Requirements
আমাদের repository store করবে:
Course code
Title
Price
Status
File layout:
data/
└── courses/
├── java-oop.properties
├── backend.properties
└── system-design.properties
Repository contract:
save(...)
findByCode(...)
findAll()
deleteByCode(...)
Additional operation:
backupTo(...)
Project Structure
src/main/java/io/liveklass/
├── Main.java
├── course/
│ ├── Course.java
│ ├── CourseCode.java
│ └── CourseStatus.java
└── repository/
├── CourseRepository.java
├── CourseRepositoryException.java
└── FileCourseRepository.java
Part 1: Course Status
CourseStatus.java
package io.liveklass.course;
public enum CourseStatus {
DRAFT,
REVIEW,
PUBLISHED,
ARCHIVED
}
Part 2: Course Code
CourseCode.java
package io.liveklass.course;
import java.util.Objects;
public final class CourseCode {
private final String value;
public CourseCode(
String value
) {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
String normalized =
value.strip()
.toUpperCase();
if (
!normalized.matches(
"[A-Z0-9-]+"
)
) {
throw new IllegalArgumentException(
"Course code contains unsupported characters."
);
}
this.value =
normalized;
}
public String getValue() {
return value;
}
@Override
public boolean equals(
Object other
) {
if (this == other) {
return true;
}
if (
!(other
instanceof CourseCode courseCode)
) {
return false;
}
return value.equals(
courseCode.value
);
}
@Override
public int hashCode() {
return Objects.hash(
value
);
}
@Override
public String toString() {
return value;
}
}
Part 3: Course Domain Object
Course.java
package io.liveklass.course;
public final class Course {
private final CourseCode code;
private final String title;
private final long priceInPaisa;
private final CourseStatus status;
public Course(
CourseCode code,
String title,
long priceInPaisa,
CourseStatus status
) {
if (code == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (priceInPaisa < 0) {
throw new IllegalArgumentException(
"Course price cannot be negative."
);
}
if (status == null) {
throw new IllegalArgumentException(
"Course status is required."
);
}
this.code = code;
this.title = title.strip();
this.priceInPaisa = priceInPaisa;
this.status = status;
}
public CourseCode getCode() {
return code;
}
public String getTitle() {
return title;
}
public long getPriceInPaisa() {
return priceInPaisa;
}
public CourseStatus getStatus() {
return status;
}
@Override
public String toString() {
return code
+ " — "
+ title
+ " — "
+ status;
}
}
Part 4: Repository Contract
CourseRepository.java
package io.liveklass.repository;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import java.nio.file.Path;
import java.util.List;
public interface CourseRepository {
void save(
Course course
);
Course findByCode(
CourseCode courseCode
);
List<Course> findAll();
boolean deleteByCode(
CourseCode courseCode
);
void backupTo(
Path backupDirectory
);
}
Part 5: Repository Exception
CourseRepositoryException.java
package io.liveklass.repository;
public final class CourseRepositoryException
extends RuntimeException {
public CourseRepositoryException(
String message
) {
super(
message
);
}
public CourseRepositoryException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
}
Part 6: File Repository
FileCourseRepository.java
package io.liveklass.repository;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseStatus;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.stream.Stream;
public final class FileCourseRepository
implements CourseRepository {
private final Path baseDirectory;
public FileCourseRepository(
Path baseDirectory
) {
if (baseDirectory == null) {
throw new IllegalArgumentException(
"Base directory is required."
);
}
this.baseDirectory =
baseDirectory
.toAbsolutePath()
.normalize();
initializeStorage();
}
@Override
public void save(
Course course
) {
if (course == null) {
throw new IllegalArgumentException(
"Course is required."
);
}
Path target =
resolveCourseFile(
course.getCode()
);
Properties properties =
serialize(
course
);
try {
replaceAtomically(
target,
properties
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not save course "
+ course.getCode()
+ ".",
exception
);
}
}
@Override
public Course findByCode(
CourseCode courseCode
) {
Path path =
resolveCourseFile(
courseCode
);
try {
return deserialize(
loadProperties(
path
),
path
);
} catch (
NoSuchFileException exception
) {
return null;
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not load course "
+ courseCode
+ ".",
exception
);
}
}
@Override
public List<Course> findAll() {
try (
Stream<Path> files =
Files.list(
baseDirectory
)
) {
return files.filter(
Files::isRegularFile
)
.filter(
this::isCourseFile
)
.sorted()
.map(
this::loadCourse
)
.toList();
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not list courses.",
exception
);
}
}
@Override
public boolean deleteByCode(
CourseCode courseCode
) {
Path path =
resolveCourseFile(
courseCode
);
try {
return Files.deleteIfExists(
path
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not delete course "
+ courseCode
+ ".",
exception
);
}
}
@Override
public void backupTo(
Path backupDirectory
) {
if (backupDirectory == null) {
throw new IllegalArgumentException(
"Backup directory is required."
);
}
Path targetRoot =
backupDirectory
.toAbsolutePath()
.normalize();
try {
Files.createDirectories(
targetRoot
);
try (
Stream<Path> paths =
Files.walk(
baseDirectory
)
) {
paths.forEach(
source -> {
Path relative =
baseDirectory.relativize(
source
);
Path target =
targetRoot.resolve(
relative
);
try {
if (
Files.isDirectory(
source
)
) {
Files.createDirectories(
target
);
} else {
Files.copy(
source,
target,
StandardCopyOption.REPLACE_EXISTING
);
}
} catch (
IOException exception
) {
throw new UncheckedIOException(
exception
);
}
}
);
}
} catch (
UncheckedIOException exception
) {
throw new CourseRepositoryException(
"Could not create course backup.",
exception.getCause()
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not create course backup.",
exception
);
}
}
private void initializeStorage() {
try {
Files.createDirectories(
baseDirectory
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not initialize course storage.",
exception
);
}
}
private Path resolveCourseFile(
CourseCode courseCode
) {
if (courseCode == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
String fileName =
courseCode.getValue()
.toLowerCase()
+ ".properties";
Path target =
baseDirectory.resolve(
fileName
)
.normalize();
if (
!target.startsWith(
baseDirectory
)
) {
throw new IllegalArgumentException(
"Course path escapes storage directory."
);
}
return target;
}
private Properties serialize(
Course course
) {
Properties properties =
new Properties();
properties.setProperty(
"code",
course.getCode()
.getValue()
);
properties.setProperty(
"title",
course.getTitle()
);
properties.setProperty(
"priceInPaisa",
Long.toString(
course.getPriceInPaisa()
)
);
properties.setProperty(
"status",
course.getStatus()
.name()
);
return properties;
}
private Course deserialize(
Properties properties,
Path source
) {
try {
CourseCode code =
new CourseCode(
requireProperty(
properties,
"code"
)
);
String title =
requireProperty(
properties,
"title"
);
long priceInPaisa =
Long.parseLong(
requireProperty(
properties,
"priceInPaisa"
)
);
CourseStatus status =
CourseStatus.valueOf(
requireProperty(
properties,
"status"
)
);
return new Course(
code,
title,
priceInPaisa,
status
);
} catch (
CourseRepositoryException exception
) {
throw exception;
} catch (
RuntimeException exception
) {
throw new CourseRepositoryException(
"Stored course data is invalid: "
+ source.getFileName()
+ ".",
exception
);
}
}
private String requireProperty(
Properties properties,
String key
) {
String value =
properties.getProperty(
key
);
if (
value == null
|| value.isBlank()
) {
throw new CourseRepositoryException(
"Stored course is missing property: "
+ key
+ "."
);
}
return value;
}
private Properties loadProperties(
Path path
) throws IOException {
Properties properties =
new Properties();
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
properties.load(
reader
);
}
return properties;
}
private void storeProperties(
Path path,
Properties properties
) throws IOException {
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
) {
properties.store(
writer,
null
);
}
}
private void replaceAtomically(
Path target,
Properties properties
) throws IOException {
Path temp =
Files.createTempFile(
baseDirectory,
"course-",
".tmp"
);
try {
storeProperties(
temp,
properties
);
try {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} catch (
AtomicMoveNotSupportedException exception
) {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING
);
}
} finally {
Files.deleteIfExists(
temp
);
}
}
private boolean isCourseFile(
Path path
) {
return path.getFileName()
.toString()
.endsWith(
".properties"
);
}
private Course loadCourse(
Path path
) {
try {
return deserialize(
loadProperties(
path
),
path
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not load stored course "
+ path.getFileName()
+ ".",
exception
);
}
}
}
Design Review
এই repository several important design decisions নেয়।
Safe Paths
Course code directly arbitrary path নয়।
CourseCode
already restricts values।
Repository still:
normalize()
startsWith(baseDirectory)
check করে।
UTF-8
Reading:
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
Writing:
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
Storage contract explicit।
Missing vs Failure
Missing file:
NoSuchFileException
becomes:
null
according to repository contract।
Other I/O failure becomes:
CourseRepositoryException
Corrupted Data
Example:
priceInPaisa=hello
does not become:
Course not found
It becomes repository failure।
Atomic Replacement
Repository first writes:
Temporary file
Then moves it over:
Actual course file
This reduces partial replacement risk।
Part 7: Application Demo
Main.java
package io.liveklass;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseStatus;
import io.liveklass.repository.CourseRepository;
import io.liveklass.repository.FileCourseRepository;
import java.nio.file.Path;
public class Main {
public static void main(
String[] args
) {
CourseRepository repository =
new FileCourseRepository(
Path.of(
"data",
"courses"
)
);
Course java =
new Course(
new CourseCode(
"JAVA-OOP"
),
"Java and OOP Foundation",
499_000L,
CourseStatus.DRAFT
);
Course backend =
new Course(
new CourseCode(
"BACKEND"
),
"Backend Development",
799_000L,
CourseStatus.REVIEW
);
repository.save(
java
);
repository.save(
backend
);
Course loaded =
repository.findByCode(
new CourseCode(
"java-oop"
)
);
System.out.println(
"Loaded:"
);
System.out.println(
loaded
);
System.out.println();
System.out.println(
"All courses:"
);
repository.findAll()
.forEach(
System.out::println
);
repository.backupTo(
Path.of(
"backup",
"courses"
)
);
boolean deleted =
repository.deleteByCode(
new CourseCode(
"BACKEND"
)
);
System.out.println();
System.out.println(
"Backend deleted: "
+ deleted
);
}
}
Expected Storage
After save:
data/
└── courses/
├── backend.properties
└── java-oop.properties
Backup:
backup/
└── courses/
├── backend.properties
└── java-oop.properties
After deleting BACKEND:
data/
└── courses/
└── java-oop.properties
The backup still contains the earlier copy।
Assessment Part 1: Choose the Correct API
Choose the best API for each case।
Question 1
Read an entire 30 KB Markdown lesson।
Options:
Files.readString()
Files.lines()
InputStream
Answer
Files.readString()
The file is small and the entire content is needed।
Question 2
Search a 5 GB log file for the first matching line।
Answer
Files.lines()
or:
BufferedReader
Streaming avoids loading the whole file।
Question 3
Copy a 500 MB PDF exactly।
Answer
Files.copy()
if no custom processing is required।
Otherwise use:
InputStream
OutputStream
Question 4
Generate one million lines incrementally।
Answer
BufferedWriter
Question 5
List direct children of a directory।
Answer
Files.list()
Question 6
Search every nested file under a directory tree।
Answer
Files.walk()
Assessment Part 2: Find the Bug
Problem 1
String path =
"data/"
+ courseCode
+ "/course.properties";
Problem
Manual path construction।
Better:
Path path =
Path.of(
"data",
courseCode,
"course.properties"
);
or use:
resolve()
Problem 2
if (
Files.exists(
path
)
) {
Files.delete(
path
);
}
Problem
Check-then-act race।
Better:
Files.deleteIfExists(
path
);
Problem 3
Stream<Path> paths =
Files.walk(
root
);
paths.forEach(
System.out::println
);
Problem
The resource-backed stream is not closed।
Better:
try (
Stream<Path> paths =
Files.walk(
root
)
) {
paths.forEach(
System.out::println
);
}
Problem 4
Files.readString(
Path.of(
"video.mp4"
)
);
Problem
Video is binary data।
Use byte-oriented APIs।
Problem 5
try (
InputStream input =
Files.newInputStream(
path
)
) {
return input;
}
Problem
Returned stream is already closed।
Problem 6
catch (
IOException exception
) {
return null;
}
inside every repository operation।
Problem
This makes:
Not found
Permission denied
Corrupted storage
Disk failure
look identical।
Assessment Part 3: Path Safety
Suppose:
Path base =
Path.of(
"/data/courses"
)
.toAbsolutePath()
.normalize();
User provides:
../../secret.txt
A safer pattern:
Path target =
base.resolve(
userInput
)
.normalize();
if (
!target.startsWith(
base
)
) {
throw new IllegalArgumentException(
"Path escapes storage root."
);
}
Why Path Validation Matters
Without validation:
/data/courses
+
../../secret.txt
can resolve outside the intended storage root।
External input should not receive uncontrolled file-system access।
Assessment Part 4: Resource Ownership
Consider:
public void process(
InputStream input
) throws IOException {
}
Caller created the stream।
Should process() close it?
Default answer:
No
unless method contract explicitly transfers ownership।
A useful rule:
Who opens the resource usually owns the resource.
Assessment Part 5: Serialization
Why does the repository write:
course.getStatus()
.name()
instead of:
course.getStatus()
.ordinal()
Because:
name() → Meaningful stable identifier
ordinal() → Depends on declaration order
Changing enum order could corrupt stored meaning if ordinal is persisted।
Assessment Part 6: Missing vs Broken Data
Suppose:
java-oop.properties
does not exist।
Possible repository result:
null
according to our contract।
Now suppose it exists but contains:
priceInPaisa=banana
Correct result:
CourseRepositoryException
The entity is not missing।
Its stored representation is invalid।
Assessment Part 7: Atomic Save
Why use:
Temporary file
↓
Write new content
↓
Move over target
instead of directly overwriting important content?
Because new data is fully written before the target is replaced।
This reduces the chance of exposing an incomplete target file।
Does Atomic Move Solve Concurrency?
No।
Suppose:
Sakib loads version 1
Jalisa loads version 1
Sakib changes title
Jalisa changes status
Sakib saves
Jalisa saves afterward
Jalisa's file may overwrite Sakib's title change।
Atomic replacement ensures each individual file replacement is complete।
It does not detect stale writers।
File Storage vs Database Assessment
Choose the more appropriate storage।
Case 1
A small command-line tool storing five settings।
Answer
File storage can be enough।
Case 2
Static Markdown lessons deployed with an application।
Answer
File or object storage can be appropriate।
Case 3
Millions of learner enrollments with concurrent writes।
Answer
Database।
Case 4
Course images and videos।
Answer
File/object storage is appropriate for the media itself।
Metadata may live in a database।
Case 5
Payments requiring transactions and uniqueness guarantees।
Answer
Database-backed transactional persistence।
Independent Practice 1: Course Content Storage
Create:
CourseContentStorage
Contract:
void write(
CourseCode courseCode,
String lessonName,
String content
);
String read(
CourseCode courseCode,
String lessonName
);
boolean delete(
CourseCode courseCode,
String lessonName
);
Requirements:
- UTF-8
- Safe base path
- Parent directory creation
- Exception translation
- Missing content distinguished from storage failure
Independent Practice 2: Recursive Backup
Implement:
void backup(
Path source,
Path target
)
Requirements:
- Recursively walk source
- Preserve relative structure
- Create directories
- Copy files
- Use try-with-resources
- Translate
IOException
Independent Practice 3: Large Export
Generate:
1,000,000 course records
as text।
Do not create one huge String।
Use:
BufferedWriter
and write incrementally।
Independent Practice 4: Safe Cleanup
Implement:
void deleteCourseDirectory(
Path storageRoot,
CourseCode courseCode
)
Requirements:
- Resolve under storage root
- Normalize
- Verify
startsWith(storageRoot) - Reject deleting the storage root itself
- Walk recursively
- Delete children before parents
Predict the Result
Question 1
Path path =
Path.of(
"courses",
"java",
"..",
"backend"
)
.normalize();
System.out.println(
path
);
Answer
Conceptually:
courses/backend
Question 2
Files.createDirectories(
existingDirectory
);
Does it fail simply because the directory exists?
Answer
Normally no।
Question 3
Files.deleteIfExists(
missingPath
);
What does it return?
Answer
false
Question 4
InputStream input =
Files.newInputStream(
path
);
int result =
input.read();
What does:
-1
mean?
Answer
End of stream।
Question 5
Does:
Files.copy(
directory,
target
);
automatically copy all descendants?
Answer
No।
Recursive directory copy requires traversal।
Question 6
What is wrong with:
Files.readAllLines(
hugeFile
);
for a 20 GB file?
Answer
It attempts to represent the whole file as an in-memory list।
Use streaming instead।
True or False
- Creating a
Pathreads the file. Files.readString()loads the complete text.Files.lines()should usually be closed.BufferedWritercan support incremental output.InputStreamis intended for text only.Files.createDirectories()can create missing parents.Files.walk()is recursive.Files.list()is recursive.deleteIfExists()is useful for cleanup.- Atomic move always works on every file system.
- File repository can hide
IOExceptionfrom business code. - Missing data and corrupted data are the same condition.
- Enum ordinal is a strong persistence format.
- Atomic replacement prevents all lost updates.
- File storage is always simpler than a database.
Answers
1. False
2. True
3. True
4. True
5. False
6. True
7. True
8. False
9. True
10. False
11. True
12. False
13. False
14. False
15. False
Final Design Challenge
Design a small storage system for course lessons।
Each course has:
courseCode
Each lesson has:
lessonId
title
Markdown content
Storage layout:
content/
└── java-oop/
├── lesson-1.md
├── lesson-2.md
└── lesson-3.md
Your design must answer:
- Which layer builds paths?
- How is path traversal prevented?
- Which encoding is used?
- How is content read?
- How is content updated safely?
- How are missing files represented?
- Which exceptions leave the storage layer?
- Who closes streams?
- How are all course lessons listed?
- How would a backup be created?
Suggested Design
Path Ownership
Storage adapter builds paths।
Domain code only provides:
CourseCode
LessonId
Path Safety
Use:
resolve()
normalize()
startsWith()
against a fixed storage root।
Encoding
Use:
StandardCharsets.UTF_8
for all Markdown text।
Reading
Typical lesson:
Files.readString()
For very large text:
BufferedReader
or:
Files.lines()
Updating
Use:
Write temporary file
Move over target
for important full-file replacements।
Missing Content
Represent intentionally as:
null
Optional
Custom not-found exception
depending on repository contract।
Do not use the same representation for general I/O failure।
Exception Boundary
Translate:
IOException
into:
CourseContentStorageException
while preserving the cause।
Resource Ownership
The component that opens a stream should normally close it।
Use:
try-with-resources
Listing Lessons
Use:
Files.list()
for direct lesson files।
Use:
Files.walk()
if nested module directories are supported।
Backup
Walk the source tree, calculate:
source.relativize(
current
)
and reproduce that path under the backup root।
Evaluation Rubric
Score each area:
0 = Missing
1 = Partially correct
2 = Correct
| Area | Score |
|---|---|
Path usage | /2 |
| Safe path resolution | /2 |
| UTF-8 handling | /2 |
| Correct read API choice | /2 |
| Correct write API choice | /2 |
| Try-with-resources | /2 |
| Byte vs character I/O | /2 |
| Directory operations | /2 |
| Recursive traversal | /2 |
| Repository abstraction | /2 |
| Exception translation | /2 |
| Missing vs failure distinction | /2 |
| Atomic replacement | /2 |
| Resource ownership | /2 |
| File-storage limitations understood | /2 |
Maximum:
30
Interpretation:
26–30 → Strong Java I/O foundation
21–25 → Good understanding
15–20 → Review resource and storage design
Below 15 → Rebuild the practice project
Module Completion Checklist
Before completing Module 6, verify that you can:
- Create
Pathobjects - Distinguish relative and absolute paths
- Use
resolve()andnormalize() - Validate paths against a storage root
- Check regular files and directories
- Read text with
Files.readString() - Read lines with
readAllLines() - Stream large files with
Files.lines() - Use
BufferedReader - Write text with
Files.writeString() - Append with
StandardOpenOption.APPEND - Create nested directories
- Use
InputStreamandOutputStream - Distinguish binary and text I/O
- Use
BufferedWriterfor incremental output - Use try-with-resources
- Explain
AutoCloseable - Explain suppressed exceptions
- Copy, move, rename, and delete files
- Use
Files.list()andFiles.walk() - Perform safe recursive cleanup
- Build a file-backed repository
- Translate low-level I/O failures
- Distinguish missing from corrupted storage
- Explain file-storage concurrency limitations
Module Summary
এই module-এ আমরা শিখেছি:
Pathfile-system location represent করেFilescommon file-system operations provide করে- Relative এবং absolute paths different semantics রাখে
resolve()path composition simplify করেnormalize()redundant path components remove করে- Untrusted paths storage root-এর মধ্যে validate করা উচিত
Files.readString()small full-text reads-এর জন্য usefulFiles.readAllLines()small line-based files-এর জন্য usefulFiles.lines()large files stream করতে পারেBufferedReadercontrolled text parsing support করেFiles.writeString()text create বা replace করতে পারেAPPENDexisting content preserve করেCREATE_NEWduplicate creation reject করতে পারেBufferedWriterlarge output incrementally লিখতে usefulInputStreamএবংOutputStreamraw byte I/OReaderএবংWritercharacter I/O- Binary data text হিসেবে process করা উচিত নয়
- Try-with-resources automatic cleanup নিশ্চিত করে
AutoCloseableresource lifecycle contract provide করে- Multiple resources reverse order-এ close হয়
- Cleanup failures suppressed exception হিসেবে preserved হতে পারে
Files.copy()এবংFiles.move()high-level operations provide করে- Rename সাধারণত move operation
Files.list()direct children return করেFiles.walk()recursive traversal support করে- Recursive delete children-first হওয়া উচিত
- File-system pre-checks concurrency guarantee করে না
- Repository storage implementation domain logic থেকে hide করতে পারে
- Missing entity এবং storage failure আলাদা রাখা উচিত
- UTF-8 encoding explicitly define করা উচিত
- Temporary-file replacement safer full-file update support করতে পারে
- Atomic replacement logical lost update prevent করে না
- File storage useful হলেও transactions, querying, and concurrent updates-এর ক্ষেত্রে database অনেক stronger
Module Complete
Module 6-এর main design principle:
Use the simplest correct I/O API,
keep resource ownership explicit,
and keep storage details behind a clear boundary.
File handling শুধু file read/write করা নয়।
Strong I/O code বুঝে:
What is being stored?
How large is it?
Who owns the resource?
What can fail?
What must remain atomic?
Which layer should know the file system?
এই thinking production-quality storage code-এর foundation।