JavaRush /Courses /Kotlin SELF /Partial results and cleanup: temp files, renaming, cleanu...

Partial results and cleanup: temp files, renaming, cleanup

Kotlin SELF
Level 56, Lesson 4
Available

1. Partial file and publishing the result

If files were honest, they would simply disappear when something goes wrong. But reality is harsher: on an error, power loss, disk full, or coroutine cancellation, we can end up with a file that exists, has a size greater than zero, sometimes even opens… and yet contains only part of the data. Such a “half-file” is especially dangerous because it looks like a normal result of the program.

Imagine two scenarios. In the first, you generate a report report.txt and write it directly. The user sees the file, opens it—there’s truncated text, but no obvious error. In the second, you copy a large file and interrupt the process—if you wrote straight to the final path, you now have a “ready” file that is actually corrupted. And that’s where fun quests start, like “why does my archive sometimes break”.

Almost always, it’s better to have a rule: a result is considered published only after the operation completes successfully, and until then it must live in a “draft” (temp/part). That’s exactly the mechanism we’re going to build.

Publishing strategy: temp → rename → cleanup

In this topic, it’s important to get the terminology straight, because it genuinely helps you think. There is the action “do the work” (write bytes/text), and there is the action “publish the result” (make it so that the user/another program sees the final file). If you mix these actions into one, you’ll get partial results that look “real”.

The strategy we’ll use is very simple at the idea level:

  1. create a temporary file next to the target (.tmp/.part);
  2. write/copy only into it;
  3. if everything succeeded—rename it to the target name;
  4. if something went wrong—in finally try to delete the temporary file (best-effort cleanup).

It’s convenient to visualize this as a small flowchart:

flowchart TD
    A[Need to produce file target] --> B[Create a temporary file nearby: target.tmp/target.part]
    B --> C[Write/copy data only into temp]
    C --> D{Success?}
    D -->|Yes| E[Rename temp -> target]
    D -->|No / cancellation| F[Do not publish the result]
    E --> G[Cleanup: delete leftover temp if any]
    F --> G

In Kotlin, the “guaranteed cleanup” discipline is usually built on try/finally. And the “guaranteed resource closing” discipline is built on .use { ... }, which automatically closes an AutoCloseable even in the presence of exceptions. This is exactly the case where the language’s standard tools help you avoid doing “magic rituals” by hand.

Why temp should be next to the target

It’s very tempting to create a temp file somewhere under /tmp, because “that’s what temp is for”. But we need predictability, not adventures. By placing temp next to the target (in the same directory), you reduce the number of surprises with permissions, different disks, and moves between file systems.

One more point is naming. .tmp or .part do two useful things: first, you can visually see “this is not final”; second, if another tool reads your folder (or even you a week later), the chance of mixing things up is smaller. This is a rare case where a “nice name” is part of reliability.

Let’s make a small table so your brain doesn’t argue with itself:

Write approach What the user sees during the process “Half-file” risk Clarity after a failure
Write directly to target.txt target.txt appears and grows high “why does the file exist but it’s broken?”
Write to target.txt.tmp, then rename target.txt is absent until success low *.tmp remains; it’s visible that the operation didn’t finish

And yes, sometimes temp can remain too. But “report.txt.tmp is left behind” is a thousand times clearer than “report.txt exists but it’s broken”.

2. Cleanup and best-effort

Why cleanup is done in finally

In an ideal world, delete() always deletes a file, and renameTo() always renames it. In the real world, your file can be locked by the system, antivirus, another program, or your own code (when you forgot to close a stream). That’s why cleanup should be best-effort: we honestly try, but we don’t turn cleanup into a separate catastrophe that masks the primary failure.

Key point: finally runs on normal completion, on exceptions, and on coroutine cancellation (cancellation ultimately also “arrives” as an exception). So if you want to “clean up no matter what”, the place for it is almost always finally. In Kotlin, try { ... } finally { ... } is valid even without catch when your goal is guaranteed finalization/cleanup actions.

deleteQuietly(...)

Below is a small helper we’ll use in the examples. It’s deliberately “quiet”: it won’t crash if deletion fails.

import java.io.File

fun deleteQuietly(file: File) {
    try {
        if (file.exists()) file.delete()
    } catch (_: Exception) {
        // Cleanup best-effort: do not turn cleanup into a new error
    }
}

There’s a bit of self-irony here: we pretend deletion is “simple”, and then we add try/catch, because life loves breaking our illusions.

3. Practice: safe writing and copying

writeTextSafely(...)

Now let’s assemble the first useful building block for our training app. Suppose we have a console utility that processes files and writes a report (log, summary, list of errors) to a text file. Writing the report directly to report.txt is dangerous: if we get canceled or crash halfway through, we leave behind a “report stub” that looks like the final output.

Let’s make a suspend function that writes to temp and then publishes the result via renameTo. It’s important that we run file operations on Dispatchers.IO, because they are blocking.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File

suspend fun writeTextSafely(target: File, text: String) {
    val tmp = File(target.parentFile, target.name + ".tmp")

    try {
        withContext(Dispatchers.IO) {
            tmp.parentFile?.mkdirs()
            tmp.writeText(text)
        }

        val renamed = withContext(Dispatchers.IO) {
            if (target.exists()) target.delete()
            tmp.renameTo(target)
        }
        if (!renamed) error("Failed to rename ${tmp.name} to ${target.name}")
    } finally {
        deleteQuietly(tmp)
    }
}

Pay attention to the intent. While we are writing, target may not exist at all (or it may remain the old one). Only after writing temp succeeds do we attempt the publish step (rename). And even if publishing fails, we still have temp, which we’ll try to clean up.

copySafely(...)

With text, things were relatively calm: writeText writes everything at once. But you can’t do that with large files (memory, cancellation, and control issues). In the previous lecture we already did chunked copying and added cooperative cancellation via isActive. Now we’ll add a second layer: do not write directly to the target file; write to .part, and only then publish.

Assume the function copyCancellable(from, to) already exists from the previous lecture and copies a file in blocks, periodically checking for cancellation. Then the safe wrapper will look like this:

import java.io.File

suspend fun copySafely(from: File, target: File) {
    val part = File(target.parentFile, target.name + ".part")

    try {
        part.parentFile?.mkdirs()
        copyCancellable(from, part) // copy into a draft

        val ok = if (target.exists()) target.delete() else true
        if (!ok) error("Failed to delete old file: ${target.name}")

        val renamed = part.renameTo(target)
        if (!renamed) error("Failed to publish the result: ${target.name}")
    } finally {
        deleteQuietly(part)
    }
}

There is an important “code psychology” here: we are not trying to make everything perfect at the file-system level (that’s a whole separate science), but we are creating a clear, practical, and almost always sufficient contract for a training project: “either there is a new correct file, or there is no new file at all”. And .part along the way will either be deleted or remain as a signal that the operation didn’t finish.

Integrating into a console app: report

Now let’s combine two ideas: we copy several files, and then write a final report so that it never appears partially. Within one lecture we’re not building a huge framework—we only need a skeleton that demonstrates the style.

For the report, let’s define a simple result model. We already know data class, so we’ll use it as a “data container without extra philosophy”.

import java.io.File

data class CopyResult(
    val from: File,
    val to: File,
    val ok: Boolean,
    val message: String
)

Let’s build a string report. To avoid going into big formatting topics, we’ll assemble plain text with line breaks.

fun buildReport(results: List<CopyResult>): String {
    val lines = results.joinToString(separator = "\n") { r ->
        val status = if (r.ok) "OK" else "ERROR"
        "${status}: ${r.from.name} -> ${r.to.name} (${r.message})"
    }
    return lines + "\n"
}

And now a “mini main” that shows the principle: get results, build text, write the report via writeTextSafely. Here we don’t re-expand the entire parallelism pipeline; we just show where exactly the “safe publishing of the result” goes.

import kotlinx.coroutines.runBlocking
import java.io.File

fun main() = runBlocking {
    val results = listOf(
        CopyResult(File("a.bin"), File("out/a.bin"), ok = true, message = "copied"),
        CopyResult(File("b.bin"), File("out/b.bin"), ok = false, message = "not found")
    )

    val reportText = buildReport(results)
    writeTextSafely(File("out/report.txt"), reportText)

    println("Report saved") // Report saved
}

The meaningful win: even if we get canceled during report writing or something crashes, we won’t leave the user with “half a report” disguised as the final document.

4. renameTo nuances

At this point a beginner usually asks: “Why check renameTo at all—doesn’t it just… rename?” And here’s the most honest answer: renameTo doesn’t promise you happiness; it promises you a Boolean. If it returns false, that’s not “weird”, that’s the contract. That’s why we turn false into an explicit error—to avoid a “silent failure” and a mysterious result.

There are several common reasons why renaming can fail. Sometimes the target file already exists and renaming does not replace it (which is why above we sometimes delete the target ahead of time). Sometimes the file is still open by a stream (which is why .use {} is so important—it guarantees the resource is closed when the block finishes). Kotlin specifically recommends the .use() idiom for closing AutoCloseable resources so you don’t keep streams open and break file operations.

Another practical nuance: if the temporary file is “somewhere else”, renaming may turn into a more complex OS-level operation. That’s why we keep temp/part next to the target: fewer surprises, more predictability.

And finally: even if you did everything right, you can still end up with a situation where cleanup did not delete the .part (for example, the file is locked). It’s unpleasant, but it’s still better than leaving a corrupted “final” file. In our contract, an “extra .part” is junk, while a “corrupted target” is a data error.

5. Typical mistakes with temp/part and cleanup

Mistake #1: writing directly to the target and believing “it probably won’t be interrupted”.
When you write straight to the final file, you’re effectively promising the world: “what’s at this path is a finished result”. On cancellation or exception, the promise is broken, and then you get the most unpleasant category of bugs: “the data looks valid, but it actually isn’t”.

Mistake #2: cleanup in catch, but not in finally.
Coroutine cancellation and many unexpected failures can bypass your specific catch (or arrive as a different exception than you expected). If cleanup must happen “no matter what”, its place is finally, because it runs on any exit from the block.

Mistake #3: ignoring the result of renameTo and pretending everything worked.
The most insidious variant: the operation did not publish, but you didn’t check the Boolean and continued as if nothing happened. The user sees the old file or no file at all, while you’re sure “we wrote it”. Checking the result and turning false into an explicit error is not paranoia—it’s adult life.

Mistake #4: the temporary file is created not next to the target.
If temp is elsewhere, you increase the chance of permission issues, different devices/disks, and unpredictable rename behavior. Next to the target is a simple rule that dramatically reduces surprises.

Mistake #5: forgetting to close the stream and wondering why you can’t delete or rename the file.
Open InputStream/OutputStream and Reader/Writer can keep a file busy. That’s why .use { ... } is not “nice style”, but a practical necessity: it closes the resource even on exceptions, acting as a guaranteed completion discipline.

1
Task
Kotlin SELF, level 56, lesson 4
Locked
Quiet cleanup
Quiet cleanup
1
Task
Kotlin SELF, level 56, lesson 4
Locked
Publishing a report
Publishing a report
1
Task
Kotlin SELF, level 56, lesson 4
Locked
Draft copy
Draft copy
1
Task
Kotlin SELF, level 56, lesson 4
Locked
Lossless update
Lossless update
1
Survey/quiz
Asynchronous I/O, 56 level, 4 lesson
Unavailable
Asynchronous I/O
Asynchronous I/O
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION