├── .github
└── workflows
│ ├── check-build.yml
│ ├── gradle-publish.yml
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── commonMain
└── kotlin
│ ├── File.common.kt
│ ├── FileTreeWalk.common.kt
│ ├── FileUtils.common.kt
│ ├── Files.kt
│ ├── exceptions.kt
│ └── platform.common.kt
├── commonTest
└── kotlin
│ └── FileTests.kt
├── jsMain
└── kotlin
│ ├── File.kt
│ └── platform.kt
├── jvmMain
└── kotlin
│ ├── File.kt
│ └── platform_common.kt
├── linuxX64Main
└── kotlin
│ ├── modified.kt
│ └── platform.kt
├── macosArm64Main
└── kotlin
│ ├── modified.kt
│ └── platform.kt
├── macosRunner
└── kotlin
│ └── runner.kt
├── macosX64Main
└── kotlin
│ ├── modified.kt
│ └── platform.kt
├── mingwX64Main
└── kotlin
│ ├── File.kt
│ └── platform.kt
└── posixMain
└── kotlin
├── File.kt
└── posix.kt
/.github/workflows/check-build.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Master Build
4 |
5 | # Controls when the action will run.
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the master branch
8 | push:
9 | branches: [ master ]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | jobs:
15 | gradle:
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | os: [ ubuntu-latest, windows-latest, macos-latest ]
20 | steps:
21 | - uses: actions/checkout@v2
22 | - uses: eskatos/gradle-command-action@v1
23 | - uses: fwilhe2/setup-kotlin@main
24 | with:
25 | install-native: true
26 |
27 | - name: Select Xcode 14
28 | if: ${{ startsWith(matrix.os, 'macos') }}
29 | run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
30 |
31 | - name: Cache Build files
32 | uses: actions/cache@v2
33 | if: ${{ !startsWith(matrix.os, 'windows') }}
34 | with:
35 | path: |
36 | ~/.konan
37 | ~/.gradle
38 | key: ${{ runner.os }}-${{ hashFiles('gradle.properties') }}
39 |
40 | - uses: eskatos/gradle-command-action@v1
41 | with:
42 | arguments: linuxX64MainKlibrary -s -i
43 | if: ${{ matrix.os == 'ubuntu-latest' }}
44 | - uses: eskatos/gradle-command-action@v1
45 | with:
46 | arguments: macosX64MainKlibrary macosArm64MainKlibrary -s -i
47 | if: ${{ matrix.os == 'macos-latest' }}
48 | - uses: eskatos/gradle-command-action@v1
49 | with:
50 | arguments: mingwX64MainKlibrary -s -i
51 | if: ${{ matrix.os == 'windows-latest' }}
52 |
--------------------------------------------------------------------------------
/.github/workflows/gradle-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
3 |
4 | name: Gradle Package
5 |
6 | on:
7 | workflow_dispatch:
8 |
9 | release:
10 | types: [created]
11 |
12 | jobs:
13 | gradle:
14 | strategy:
15 | matrix:
16 | os: [macos-latest, windows-latest]
17 | runs-on: ${{ matrix.os }}
18 |
19 | permissions:
20 | contents: read
21 | packages: write
22 |
23 | steps:
24 | - uses: actions/checkout@v2
25 | - uses: actions/setup-java@v2.3.0
26 | with:
27 | distribution: 'adopt-hotspot'
28 | java-version: 11
29 |
30 | - uses: actions/cache@v2.1.6
31 | with:
32 | path: ~/.gradle/caches
33 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
34 | restore-keys: |
35 | ${{ runner.os }}-gradle-
36 |
37 | - name: Select Xcode 14
38 | if: ${{ startsWith(matrix.os, 'macos') }}
39 | run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
40 |
41 | - name: Build with Gradle [Macos]
42 | uses: eskatos/gradle-command-action@v1
43 | if: ${{ startsWith(matrix.os, 'macos') }}
44 | with:
45 | arguments: clean build -x :jsNodeTest -i -s
46 |
47 | - name: Build with Gradle [Windows]
48 | uses: eskatos/gradle-command-action@v1
49 | if: ${{ startsWith(matrix.os, 'macos') }}
50 | with:
51 | arguments: clean build -x :jsNodeTest -x compilePosix -i -s
52 |
53 | - name: Prepare publications posix
54 | if: ${{ startsWith(matrix.os, 'macos') }}
55 | env:
56 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
57 | GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
58 | GPG_RINGFILE: ${{ secrets.GPG_RINGFILE }}
59 | GPG_SECRET_CONTENT: ${{ secrets.GPG_KEY_CONTENTS }}
60 | run: |
61 | sudo bash -c "echo '$GPG_SECRET_CONTENT' | base64 -d > '$GPG_RINGFILE'"
62 | bash -c "echo 'signing.keyId=$GPG_KEY_ID' >> gradle.properties"
63 | bash -c "echo 'signing.password=$GPG_PASSWORD' >> gradle.properties"
64 | bash -c "echo 'signing.secretKeyRingFile=$GPG_RINGFILE' >> gradle.properties"
65 |
66 | - name: Prepare publications mingw
67 | if: ${{ startsWith(matrix.os, 'windows') }}
68 | env:
69 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
70 | GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
71 | GPG_RINGFILE: ${{ secrets.GPG_RINGFILE }}
72 | GPG_SECRET_CONTENT: ${{ secrets.GPG_KEY_CONTENTS }}
73 | run: |
74 | $Env:GPG_SECRET_CONTENT | Out-File encodedContent.ring
75 | certutil -decode encodedContent.ring $Env:GPG_RINGFILE
76 | echo signing.keyId=$Env:GPG_KEY_ID >> gradle.properties
77 | echo signing.password=$Env:GPG_PASSWORD >> gradle.properties
78 | echo signing.secretKeyRingFile=$Env:GPG_RINGFILE >> gradle.properties
79 |
80 | - uses: eskatos/gradle-command-action@v1
81 | name: Publish targets to maven
82 | with:
83 | arguments: publish -d -s -PapiKey=${{ secrets.SONOTYPE_TOKEN }} '-Dorg.gradle.jvmargs=-Xmx2g -XX:-UseGCOverheadLimit -Dfile.encoding=UTF-8'
84 | if: ${{ startsWith(matrix.os, 'macos') }}
85 |
86 | - uses: eskatos/gradle-command-action@v1
87 | name: Publish targets to maven
88 | with:
89 | arguments: publishMingwX64PublicationToMavenRepository -s -d -x compilePosix -PapiKey=${{ secrets.SONOTYPE_TOKEN }} '-Dorg.gradle.jvmargs=-Xmx2g -XX:-UseGCOverheadLimit -Dfile.encoding=UTF-8'
90 | if: ${{ startsWith(matrix.os, 'windows') }}
91 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | gradle:
11 | strategy:
12 | matrix:
13 | os: [macos-latest, ubuntu-latest, windows-latest]
14 | runs-on: ${{ matrix.os }}
15 | steps:
16 | - uses: actions/checkout@v1
17 | - uses: actions/setup-java@v1
18 | with:
19 | java-version: 11
20 | - name: Cache Build files
21 | uses: actions/cache@v2
22 | if: ${{ !startsWith(matrix.os, 'windows') }}
23 | with:
24 | path: |
25 | ~/.konan
26 | ~/.gradle
27 | key: ${{ runner.os }}-${{ hashFiles('gradle.properties') }}
28 | - name: Select Xcode 14
29 | if: ${{ startsWith(matrix.os, 'macos') }}
30 | run: sudo xcode-select -s /Applications/Xcode_14.3.1.app/Contents/Developer
31 | - uses: eskatos/gradle-command-action@v1
32 | name: Test Windows Target
33 | if: ${{ startsWith(matrix.os, 'windows') }}
34 | with:
35 | arguments: clean mingwX64Test
36 | - uses: eskatos/gradle-command-action@v1
37 | name: Test Apple Target
38 | if: ${{ startsWith(matrix.os, 'macos') }}
39 | with:
40 | arguments: clean macosX64Test macosArm64Test
41 | - uses: eskatos/gradle-command-action@v1
42 | name: Test Linux Target
43 | if: ${{ startsWith(matrix.os, 'ubuntu') }}
44 | with:
45 | arguments: clean jvmTest linuxX64Test
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | /build/
4 |
5 | # Ignore Gradle GUI config
6 | gradle-app.setting
7 |
8 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
9 | !gradle-wrapper.jar
10 |
11 | # Cache of project
12 | .gradletasknamecache
13 |
14 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
15 | # gradle/wrapper/gradle-wrapper.properties
16 |
17 | local.properties
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://kotlinlang.org) 
2 |
3 |
4 | # native-file-io
5 | File IO library based on Posix API for Kotlin/Native
6 |
7 | Currently, contains only JS, JVM, Windows and Posix (Linux X64 / MacOS X64) actual realisation.
8 | In plans support mingw (Windows) archetype.
9 |
10 | This library shares standard java file API to native environment, implementing Posix API.
11 |
12 | # how to use it
13 |
14 | ```kotlin
15 | // put this block somewhere in root build.gradle.kts file
16 |
17 | allprojects {
18 | repositories {
19 | mavenCentral()
20 | }
21 | }
22 |
23 | // then in module's build.gradle.kts in target's dependencies section:
24 | val fileIoVersion: String by extra // reads from gradle.properties
25 |
26 | // expect; for kotlin common modules
27 | implementation("me.archinamon:file-io:$fileIoVersion")
28 |
29 | // actual; demands on target type
30 | implementation("me.archinamon:file-io-jvm:$fileIoVersion") // for jvm module
31 | implementation("me.archinamon:file-io-js:$fileIoVersion") // for kotlin-js module
32 | implementation("me.archinamon:file-io-linuxx64:$fileIoVersion") // for linux x64 posix module
33 | implementation("me.archinamon:file-io-macosx64:$fileIoVersion") // for macOS x64 posix module
34 | implementation("me.archinamon:file-io-mingwx64:$fileIoVersion") // for windows x64 module
35 | ```
36 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform") version "1.8.20"
3 | id("org.jetbrains.dokka") version "1.4.32"
4 | id("maven-publish")
5 | id("signing")
6 | }
7 |
8 | group = "me.archinamon"
9 | version = "1.3.10"
10 |
11 | val isRunningInIde: Boolean = System.getProperty("idea.active")
12 | ?.toBoolean() == true
13 |
14 | val testApp: String? by extra
15 |
16 | repositories {
17 | mavenCentral()
18 | }
19 |
20 | kotlin {
21 | js(IR) { nodejs() }
22 |
23 | jvm()
24 |
25 | // generic linux code
26 | linuxX64()
27 |
28 | // darwin macos code
29 | macosX64() {
30 | if (testApp?.toBoolean() == true) {
31 | binaries {
32 | executable()
33 | }
34 | }
35 | }
36 |
37 | macosArm64 {
38 | if (testApp?.toBoolean() == true) {
39 | binaries {
40 | executable()
41 | }
42 | }
43 | }
44 |
45 | mingwX64()
46 |
47 | sourceSets {
48 | val commonMain by getting {
49 | dependencies {
50 | implementation(kotlin("test-common"))
51 | implementation(kotlin("test-annotations-common"))
52 | }
53 | }
54 | val posixMain by creating {
55 | dependsOn(commonMain)
56 | }
57 | val macosArm64Main by getting {
58 | dependsOn(posixMain)
59 | if (testApp?.toBoolean() == true) {
60 | kotlin.srcDirs("src/macosRunner/kotlin")
61 | }
62 | }
63 | val macosX64Main by getting {
64 | dependsOn(posixMain)
65 | if (testApp?.toBoolean() == true) {
66 | kotlin.srcDirs("src/macosRunner/kotlin")
67 | }
68 | }
69 | val linuxX64Main by getting {
70 | dependsOn(posixMain)
71 | }
72 | val jvmTest by getting {
73 | dependencies {
74 | implementation(kotlin("test"))
75 | implementation(kotlin("test-junit"))
76 | }
77 | }
78 | val jsTest by getting {
79 | dependencies {
80 | implementation(kotlin("test"))
81 | implementation(kotlin("test-js"))
82 | }
83 | }
84 | }
85 | }
86 |
87 | val dokkaOutputDir = "$buildDir/dokka"
88 |
89 | tasks.dokkaHtml {
90 | outputDirectory.set(file(dokkaOutputDir))
91 | }
92 |
93 | val deleteDokkaOutputDir by tasks.registering(Delete::class) {
94 | delete(dokkaOutputDir)
95 | }
96 |
97 | val javadocJar by tasks.registering(Jar::class) {
98 | dependsOn(deleteDokkaOutputDir, tasks.dokkaHtml)
99 | archiveClassifier.set("javadoc")
100 | from(dokkaOutputDir)
101 | }
102 |
103 | publishing {
104 | publications.withType {
105 | artifact(javadocJar)
106 |
107 | pom {
108 | name.set("file-io")
109 | description.set("Kotlin/Native file IO library with standard java-io interface")
110 | url.set("https://github.com/Archinamon/native-file-io")
111 | licenses {
112 | license {
113 | name.set("MIT License")
114 | url.set("https://github.com/Archinamon/native-file-io")
115 | distribution.set("repo")
116 | }
117 | license {
118 | name.set("The Apache License, Version 2.0")
119 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
120 | }
121 | }
122 | developers {
123 | developer {
124 | id.set("Archinamon")
125 | name.set("Eduard Obolenskiy")
126 | email.set("archinamon@gmail.com")
127 | }
128 | }
129 | scm {
130 | url.set("https://github.com/Archinamon/native-file-io")
131 | connection.set("scm:git:https://github.com/Archinamon/native-file-io.git")
132 | developerConnection.set("scm:git:git@github.com:Archinamon/native-file-io.git")
133 | }
134 | }
135 | }
136 |
137 | repositories {
138 | if (isRunningInIde)
139 | return@repositories
140 |
141 | val isSnapshotPublishing: String? by extra
142 | val repositoryUrl = if (isSnapshotPublishing?.toBoolean() == true)
143 | "https://s01.oss.sonatype.org/content/repositories/snapshots/"
144 | else "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
145 |
146 | maven(repositoryUrl) {
147 | credentials {
148 | val apiKey: String? by extra
149 |
150 | username = "Archinamon"
151 | password = apiKey ?: "[empty token]"
152 | }
153 | }
154 | }
155 | }
156 |
157 | signing {
158 | sign(publishing.publications)
159 | }
160 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | kotlin.js.generate.executable.default=false
3 | kotlin.js.compiler=ir
4 |
5 | kotlin.mpp.stability.nowarn=true
6 | kotlin.mpp.enableGranularSourceSetsMetadata=true
7 | kotlin.native.enableDependencyPropagation=false
8 | kotlin.native.ignoreDisabledTargets=true
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Archinamon/native-file-io/e5c7d10166a90f2ad3d4ec55cb10a3c503151d3d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=`expr $i + 1`
158 | done
159 | case $i in
160 | 0) set -- ;;
161 | 1) set -- "$args0" ;;
162 | 2) set -- "$args0" "$args1" ;;
163 | 3) set -- "$args0" "$args1" "$args2" ;;
164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=`save "$@"`
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | exec "$JAVACMD" "$@"
184 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 | @rem Execute Gradle
88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
89 |
90 | :end
91 | @rem End local scope for the variables with windows NT shell
92 | if "%ERRORLEVEL%"=="0" goto mainEnd
93 |
94 | :fail
95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
96 | rem the _cmd.exe /c_ return code!
97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
98 | exit /b 1
99 |
100 | :mainEnd
101 | if "%OS%"=="Windows_NT" endlocal
102 |
103 | :omega
104 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | rootProject.name = "file-io"
3 |
4 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/File.common.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | expect class File(pathname: String) {
4 | fun getParent(): String?
5 | fun getParentFile(): File?
6 |
7 | fun getName(): String
8 |
9 | fun lastModified(): Long
10 | fun mkdir(): Boolean
11 | fun mkdirs(): Boolean
12 | fun createNewFile(): Boolean
13 |
14 | fun isFile(): Boolean
15 | fun isDirectory(): Boolean
16 |
17 | fun getPath(): String
18 | fun getAbsolutePath(): String
19 | fun length(): Long
20 |
21 | fun exists(): Boolean
22 | fun canRead(): Boolean
23 | fun canWrite(): Boolean
24 |
25 | fun list(): Array
26 | fun listFiles(): Array
27 |
28 | fun delete(): Boolean
29 | }
30 |
31 | expect val filePathSeparator: Char
32 |
33 | fun File.asParent(child: String): File {
34 | val path = getAbsolutePath() + filePathSeparator + child
35 | return File(path)
36 | }
37 |
38 | val File.nameWithoutExtension: String
39 | get() = getName().substringBeforeLast(".")
40 |
41 | expect val File.mimeType: String
42 |
43 | expect fun File.readBytes(): ByteArray
44 |
45 | expect fun File.readText(): String
46 |
47 | expect fun File.appendText(text: String)
48 |
49 | expect fun File.appendBytes(bytes: ByteArray)
50 |
51 | expect fun File.writeText(text: String)
52 |
53 | fun File.deleteRecursively(): Boolean = walkBottomUp()
54 | .fold(initial = true) { res, it ->
55 | (it.delete() || !it.exists()) && res
56 | }
57 |
58 | fun File.getParentFileUnsafe(): File {
59 | return getParentFile()
60 | ?: getAbsolutePath()
61 | .substringBeforeLast(filePathSeparator)
62 | .run(::File)
63 | }
64 |
65 | fun File.validate() = run {
66 | print("Validating $nameWithoutExtension file...")
67 |
68 | if (!exists()) {
69 | println(); throw FileNotFoundException(getAbsolutePath(), "No such file or directory!")
70 | } else if (!canRead()) {
71 | println(); throw IllegalFileAccess(getAbsolutePath(), "Read access not granted!")
72 | } else if (!canWrite()) {
73 | println(); throw IllegalFileAccess(getAbsolutePath(), "Write access not granted!")
74 | }
75 |
76 | println(" OK!")
77 | }
78 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/FileTreeWalk.common.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | // copy from kotlin.io jvm
4 |
5 | /**
6 | * An enumeration to describe possible walk directions.
7 | * There are two of them: beginning from parents, ending with children,
8 | * and beginning from children, ending with parents. Both use depth-first search.
9 | */
10 | enum class FileWalkDirection {
11 | /** Depth-first search, directory is visited BEFORE its files */
12 | TOP_DOWN,
13 | /** Depth-first search, directory is visited AFTER its files */
14 | BOTTOM_UP
15 | }
16 |
17 | /**
18 | * This class is intended to implement different file traversal methods.
19 | * It allows to iterate through all files inside a given directory.
20 | *
21 | * Use [File.walk], [File.walkTopDown] or [File.walkBottomUp] extension functions to instantiate a `FileTreeWalk` instance.
22 |
23 | * If the file path given is just a file, walker iterates only it.
24 | * If the file path given does not exist, walker iterates nothing, i.e. it's equivalent to an empty sequence.
25 | */
26 | class FileTreeWalk private constructor(
27 | private val start: File,
28 | private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
29 | private val onEnter: ((File) -> Boolean)?,
30 | private val onLeave: ((File) -> Unit)?,
31 | private val onFail: ((f: File, e: FileIOException) -> Unit)?,
32 | private val maxDepth: Int = Int.MAX_VALUE
33 | ) : Sequence {
34 |
35 | internal constructor(start: File, direction: FileWalkDirection = FileWalkDirection.TOP_DOWN) :
36 | this(start, direction, null, null, null)
37 |
38 | /** Returns an iterator walking through files. */
39 | override fun iterator(): Iterator = FileTreeWalkIterator()
40 |
41 | /** Abstract class that encapsulates file visiting in some order, beginning from a given [root] */
42 | private abstract class WalkState(val root: File) {
43 | /** Call of this function proceeds to a next file for visiting and returns it */
44 | abstract fun step(): File?
45 | }
46 |
47 | /** Abstract class that encapsulates directory visiting in some order, beginning from a given [rootDir] */
48 | private abstract class DirectoryState(rootDir: File) : WalkState(rootDir)
49 |
50 | private inner class FileTreeWalkIterator : AbstractIterator() {
51 |
52 | // Stack of directory states, beginning from the start directory
53 | private val state = ArrayDeque()
54 |
55 | init {
56 | when {
57 | start.isDirectory() -> state.addLast(directoryState(start))
58 | start.isFile() -> state.addLast(SingleFileState(start))
59 | else -> done()
60 | }
61 | }
62 |
63 | override fun computeNext() {
64 | val nextFile = gotoNext()
65 | if (nextFile != null)
66 | setNext(nextFile)
67 | else
68 | done()
69 | }
70 |
71 |
72 | private fun directoryState(root: File): DirectoryState {
73 | return when (direction) {
74 | FileWalkDirection.TOP_DOWN -> TopDownDirectoryState(root)
75 | FileWalkDirection.BOTTOM_UP -> BottomUpDirectoryState(root)
76 | }
77 | }
78 |
79 | private tailrec fun gotoNext(): File? {
80 | // Take next file from the top of the stack or return if there's nothing left
81 | val topState = state.lastOrNull() ?: return null
82 | val file = topState.step()
83 | if (file == null) {
84 | // There is nothing more on the top of the stack, go back
85 | state.removeLast()
86 | return gotoNext()
87 | } else {
88 | // Check that file/directory matches the filter
89 | return if (file == topState.root || !file.isDirectory() || state.size >= maxDepth) {
90 | // Proceed to a root directory or a simple file
91 | file
92 | } else {
93 | // Proceed to a sub-directory
94 | state.addLast(directoryState(file))
95 | gotoNext()
96 | }
97 | }
98 | }
99 |
100 | /** Visiting in bottom-up order */
101 | private inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) {
102 |
103 | private var rootVisited = false
104 |
105 | private var fileList: Array? = null
106 |
107 | private var fileIndex = 0
108 |
109 | private var failed = false
110 |
111 | /** First all children, then root directory */
112 | override fun step(): File? {
113 | if (!failed && fileList == null) {
114 | if (onEnter?.invoke(root) == false) {
115 | return null
116 | }
117 |
118 | fileList = root.listFiles()
119 | if (fileList == null) {
120 | onFail?.invoke(root, IllegalFileAccess(fileName = root.getName(), reason = "Cannot list files in a directory"))
121 | failed = true
122 | }
123 | }
124 | return when {
125 | fileList != null && fileIndex < fileList!!.size -> {
126 | // First visit all files
127 | fileList!![fileIndex++]
128 | }
129 |
130 | !rootVisited -> {
131 | // Then visit root
132 | rootVisited = true
133 | root
134 | }
135 |
136 | else -> {
137 | // That's all
138 | onLeave?.invoke(root)
139 | null
140 | }
141 | }
142 | }
143 | }
144 |
145 | /** Visiting in top-down order */
146 | private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {
147 |
148 | private var rootVisited = false
149 |
150 | private var fileList: Array? = null
151 |
152 | private var fileIndex = 0
153 |
154 | /** First root directory, then all children */
155 | override fun step(): File? {
156 | if (!rootVisited) {
157 | // First visit root
158 | if (onEnter?.invoke(root) == false) {
159 | return null
160 | }
161 |
162 | rootVisited = true
163 | return root
164 | } else if (fileList == null || fileIndex < fileList!!.size) {
165 | if (fileList == null) {
166 | // Then read an array of files, if any
167 | fileList = root.listFiles()
168 | if (fileList == null) {
169 | onFail?.invoke(root, IllegalFileAccess(fileName = root.getName(), reason = "Cannot list files in a directory"))
170 | }
171 | if (fileList == null || fileList!!.isEmpty()) {
172 | onLeave?.invoke(root)
173 | return null
174 | }
175 | }
176 | // Then visit all files
177 | return fileList!![fileIndex++]
178 | } else {
179 | // That's all
180 | onLeave?.invoke(root)
181 | return null
182 | }
183 | }
184 | }
185 |
186 | private inner class SingleFileState(rootFile: File) : WalkState(rootFile) {
187 | private var visited: Boolean = false
188 |
189 | override fun step(): File? {
190 | if (visited) return null
191 | visited = true
192 | return root
193 | }
194 | }
195 |
196 | }
197 |
198 | /**
199 | * Sets a predicate [function], that is called on any entered directory before its files are visited
200 | * and before it is visited itself.
201 | *
202 | * If the [function] returns `false` the directory is not entered and neither it nor its files are visited.
203 | */
204 | fun onEnter(function: (File) -> Boolean): FileTreeWalk {
205 | return FileTreeWalk(start, direction, onEnter = function, onLeave = onLeave, onFail = onFail, maxDepth = maxDepth)
206 | }
207 |
208 | /**
209 | * Sets a callback [function], that is called on any left directory after its files are visited and after it is visited itself.
210 | */
211 | fun onLeave(function: (File) -> Unit): FileTreeWalk {
212 | return FileTreeWalk(start, direction, onEnter = onEnter, onLeave = function, onFail = onFail, maxDepth = maxDepth)
213 | }
214 |
215 | /**
216 | * Set a callback [function], that is called on a directory when it's impossible to get its file list.
217 | *
218 | * [onEnter] and [onLeave] callback functions are called even in this case.
219 | */
220 | fun onFail(function: (File, FileIOException) -> Unit): FileTreeWalk {
221 | return FileTreeWalk(start, direction, onEnter = onEnter, onLeave = onLeave, onFail = function, maxDepth = maxDepth)
222 | }
223 |
224 | /**
225 | * Sets the maximum [depth] of a directory tree to traverse. By default there is no limit.
226 | *
227 | * The value must be positive and [Int.MAX_VALUE] is used to specify an unlimited depth.
228 | *
229 | * With a value of 1, walker visits only the origin directory and all its immediate children,
230 | * with a value of 2 also grandchildren, etc.
231 | */
232 | fun maxDepth(depth: Int): FileTreeWalk {
233 | if (depth <= 0) throw IllegalArgumentException("depth must be positive, but was $depth.")
234 | return FileTreeWalk(start, direction, onEnter, onLeave, onFail, depth)
235 | }
236 | }
237 |
238 | /**
239 | * Gets a sequence for visiting this directory and all its content.
240 | *
241 | * @param direction walk direction, top-down (by default) or bottom-up.
242 | */
243 | fun File.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileTreeWalk =
244 | FileTreeWalk(this, direction)
245 |
246 | /**
247 | * Gets a sequence for visiting this directory and all its content in top-down order.
248 | * Depth-first search is used and directories are visited before all their files.
249 | */
250 | fun File.walkTopDown(): FileTreeWalk = walk(FileWalkDirection.TOP_DOWN)
251 |
252 | /**
253 | * Gets a sequence for visiting this directory and all its content in bottom-up order.
254 | * Depth-first search is used and directories are visited after all their files.
255 | */
256 | fun File.walkBottomUp(): FileTreeWalk = walk(FileWalkDirection.BOTTOM_UP)
--------------------------------------------------------------------------------
/src/commonMain/kotlin/FileUtils.common.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | fun File.copyTo(dest: String, overwrite: Boolean) {
4 | this.copyTo(File(dest), overwrite)
5 | }
6 |
7 | fun File.copyTo(dest: File, overwrite: Boolean) {
8 | if (this.isDirectory()) {
9 | throw UnsupportedOperationException("Moving/copying directories directly not allowed!")
10 | }
11 |
12 | this.readBytes().let { bytes ->
13 | dest.apply {
14 | if (exists() && !overwrite) {
15 | throw IllegalFileAccess(dest.getAbsolutePath(), "Already exists and not allowed to be overwritten!")
16 | }
17 |
18 | if (!exists()) {
19 | createNewFile()
20 | }
21 |
22 | writeBytes(bytes)
23 | }
24 | }
25 | }
26 |
27 | fun File.moveTo(dest: String, overwrite: Boolean) {
28 | this.copyTo(dest, overwrite)
29 | this.delete()
30 | }
31 |
32 | fun File.moveTo(dest: File, overwrite: Boolean) {
33 | this.copyTo(dest, overwrite)
34 | this.delete()
35 | }
36 |
37 | expect fun File.writeBytes(bytes: ByteArray)
--------------------------------------------------------------------------------
/src/commonMain/kotlin/Files.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | object Files {
4 | private const val tempDirectory = "/tmp"
5 | private const val tempFileType = ".tmp"
6 |
7 | fun createTempFile(prefix: String, suffix: String? = null): File {
8 | return createTempFile(prefix, suffix, File(tempDirectory))
9 | }
10 |
11 | fun createTempFile(prefix: String, suffix: String? = null, dir: File): File {
12 | if (platform() == Platform.Windows) {
13 | throw UnsupportedOperationException("Do not supported on mingw yet, create tmp files/dirs manually!")
14 | }
15 |
16 | val parent = dir.getPath()
17 | dir.mkdirs()
18 |
19 | if (!dir.canWrite()) {
20 | throw IllegalFileAccess(parent, "Can't create file in the directory")
21 | }
22 |
23 | if (prefix.length < 3) {
24 | throw IllegalArgumentException("prefix should be at least 3 chars long, now — ${prefix.length}")
25 | }
26 |
27 | val end = suffix ?: tempFileType
28 | return File("$parent/$prefix$end").apply {
29 | createNewFile()
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/commonMain/kotlin/exceptions.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | open class FileIOException(fileName: String, reason: String) : Exception("FileIOException: $fileName ($reason)")
4 |
5 | class FileNotFoundException(fileName: String, reason: String) : FileIOException(fileName, "FileNotFoundException: ($reason)")
6 |
7 | class IllegalFileAccess(fileName: String, reason: String) : FileIOException(fileName, "Access denied: ($reason)")
--------------------------------------------------------------------------------
/src/commonMain/kotlin/platform.common.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | enum class Platform {
4 | Linux, Macos, Windows, JVM, JS;
5 |
6 | fun isPosix(): Boolean = this == Macos || this == Linux
7 | }
8 |
9 | expect fun platform(): Platform
--------------------------------------------------------------------------------
/src/commonTest/kotlin/FileTests.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlin.test.*
4 |
5 | class FileTests {
6 |
7 | @Test
8 | fun testNonexistentRootFile() {
9 | val testFile = File("testNonexistentRootFile.txt")
10 |
11 | assertFalse(testFile.exists(), "file should not exist")
12 | assertFalse(testFile.isDirectory(), "file should not be directory")
13 | assertFalse(testFile.isFile(), "file should not be file")
14 |
15 | if (platform() == Platform.JVM) {
16 | assertNull(testFile.getParent(), "file should not have parent")
17 | assertNull(testFile.getParentFile(), "file should not have parent file")
18 | }
19 |
20 | if (platform().isPosix()) { // in posix we always resolve relative path via `realpath` syscall
21 | assertEquals(testFile.getParent(), File("./").getAbsolutePath(), "as parent should be current dir")
22 | assertEquals(testFile.getParentFile()?.getAbsolutePath(), File("./").getAbsolutePath(), "as parent should be current dir")
23 | }
24 |
25 | assertEquals("testNonexistentRootFile", testFile.nameWithoutExtension)
26 | }
27 |
28 | @Test
29 | fun testExistentRootFile() {
30 | val testFile = File("testFileRoot/testExistentRootFile.txt")
31 |
32 | assertFalse(testFile.exists(), "file should not exist")
33 | assertFalse(testFile.getParentFile()?.exists() == true, "file should not have parent file")
34 | assertNotNull(testFile.getParentFileUnsafe(), "file should not have parent file")
35 |
36 | assertEquals("testFileRoot", testFile.getParentFileUnsafe().getName(), "couldn't get parent file name")
37 | }
38 |
39 | @Test
40 | fun testFileCreateAndDelete() {
41 | if (platform() == Platform.Windows) {
42 | return
43 | }
44 |
45 | val testFolder = File("build/testNewDirectoryCreation")
46 |
47 | assertTrue(testFolder.mkdirs(), "create directory failed")
48 | assertTrue(testFolder.isDirectory(), "isDirectory check failed")
49 |
50 | val testFile = File("build/testNewDirectoryCreation/test.txt")
51 | assertTrue(testFile.createNewFile(), "create file failed")
52 | assertTrue(testFile.exists(), "file should exist")
53 | assertTrue(testFile.canRead(), "file should be readable")
54 | assertTrue(testFile.canWrite(), "file should be writable")
55 | assertTrue(testFile.isFile(), "file should be considered a file")
56 | assertFalse(testFile.isDirectory(), "file should not be considered a directory")
57 | assertTrue(testFile.delete(), "delete file failed")
58 |
59 | assertTrue(testFolder.delete(), "delete directory failed")
60 | }
61 |
62 | @Test
63 | fun testFileWriteAndRead() {
64 | if (platform() == Platform.Windows) {
65 | return
66 | }
67 |
68 | val testFolder = File("build/testFileWriteAndRead")
69 | val testFile = File("build/testFileWriteAndRead/test.txt")
70 |
71 | assertTrue(testFolder.mkdirs(), "failed to create directories")
72 | assertTrue(testFile.createNewFile(), "failed to create file")
73 |
74 | val message1 = "Hello,"
75 | val message2 = " World!"
76 | testFile.writeText(message1)
77 |
78 | assertEquals(message1, testFile.readText(), "written text should match")
79 |
80 | testFile.appendText(message2)
81 |
82 | assertEquals(message1 + message2, testFile.readText(), "appended text should match")
83 |
84 | assertTrue(testFile.delete(), "failed to cleanup test file")
85 | assertTrue(testFolder.delete(), "failed to cleanup directory")
86 | }
87 |
88 | @Test
89 | fun testFileLists() {
90 | val testFolder = File("gradle/wrapper")
91 | val listedFiles = testFolder.listFiles().sortedBy { it.getName().length }
92 | val listedFileNames = testFolder.list().sortedBy { it.length }
93 | assertEquals(2, listedFiles.size, "required two files")
94 | assertEquals(2, listedFileNames.size, "required two file names")
95 | assertEquals("gradle-wrapper.jar", listedFileNames.first())
96 | assertEquals("gradle-wrapper.jar", listedFiles.first().getName())
97 | assertEquals("gradle-wrapper.properties", listedFileNames[1])
98 | assertEquals("gradle-wrapper.properties", listedFiles[1].getName())
99 | }
100 |
101 | @Test
102 | fun testFileCopyMethod() {
103 | if (platform() == Platform.Windows) {
104 | return
105 | }
106 |
107 | val testFile = File("gradle/wrapper/gradle-wrapper.properties")
108 | val testDestFolder = File("build/testCopyFolder")
109 | val testDestFile = File("build/testCopyFolder/gradle-wrapper.properties")
110 |
111 | assertTrue(testFile.exists(), "file have to exist")
112 | assertFalse(testDestFolder.exists(), "folder shouldn't be created yet")
113 |
114 | testDestFolder.mkdirs()
115 | assertTrue(testDestFolder.exists(), "now folder have to exists")
116 |
117 | assertFalse(testDestFile.exists(), "file should not exists yet")
118 | testFile.copyTo(testDestFile, overwrite = false)
119 | assertTrue(testDestFile.exists(), "file have to exist after coping")
120 |
121 | assertTrue(testDestFile.delete(), "failed to cleanup test file")
122 | assertTrue(testDestFolder.delete(), "failed to cleanup directory")
123 | }
124 |
125 | @Test
126 | fun testFileMoveMethod() {
127 | if (platform() == Platform.Windows) {
128 | return
129 | }
130 |
131 | val testFolder = File("build/testMoveFolder")
132 | val testDestFolder = File("build/testMoveFolder2")
133 | val testFile = File("build/testMoveFolder/test_move_file.properties")
134 | val testDestFile = File("build/testMoveFolder2/test_move_file.properties")
135 |
136 | assertFalse(testFile.exists(), "file have not to exist")
137 | assertFalse(testFolder.exists(), "folder shouldn't be created yet")
138 | assertFalse(testDestFolder.exists(), "folder shouldn't be created yet")
139 |
140 | assertTrue(testFolder.mkdirs() && testFolder.exists(), "now folder1 have to exists")
141 | assertTrue(testDestFolder.mkdirs() && testDestFolder.exists(), "now folder2 have to exists")
142 |
143 | assertTrue(testFile.createNewFile(), "file should be created")
144 | testFile.writeText("moving=true")
145 |
146 | testFile.moveTo(testDestFile, overwrite = false)
147 | assertTrue(testDestFile.exists(), "file have to exist after coping")
148 | assertEquals("moving=true", testDestFile.readText(), "moved file should have the same content")
149 | assertFalse(testFile.exists(), "file should not exists on previous place after moving")
150 |
151 | assertTrue(testFolder.delete(), "failed to cleanup test file")
152 | assertTrue(testDestFile.delete(), "failed to cleanup test file")
153 | assertTrue(testDestFolder.delete(), "failed to cleanup directory")
154 | }
155 |
156 | @Test
157 | fun testCreateTempFileAndDelete() {
158 | if (platform() == Platform.Windows) {
159 | return
160 | }
161 |
162 | val testFile = Files.createTempFile("test")
163 |
164 | assertContains(
165 | testFile.getAbsolutePath(),
166 | "/tmp/test.tmp".replace('/', filePathSeparator),
167 | message = "different path: ${testFile.getAbsolutePath()}"
168 | )
169 | assertTrue(testFile.exists(), "file should exist")
170 | assertTrue(testFile.canRead(), "file should be readable")
171 | assertTrue(testFile.canWrite(), "file should be writable")
172 | assertTrue(testFile.isFile(), "file should be considered a file")
173 | assertFalse(testFile.isDirectory(), "file should not be considered a directory")
174 | assertTrue(testFile.delete(), "delete file failed")
175 | }
176 |
177 | @Test
178 | fun testCreateTempFileWithinCustomDirAndDelete() {
179 | if (platform() == Platform.Windows) {
180 | return
181 | }
182 |
183 | val testDir = File("/tmp/testdir").apply { mkdirs() }
184 | val testFile = Files.createTempFile(prefix = "test", suffix = "all.t", dir = testDir)
185 |
186 | assertContains(
187 | testFile.getAbsolutePath(),
188 | "/tmp/testdir/testall.t".replace('/', filePathSeparator),
189 | message = "different path: ${testFile.getAbsolutePath()}"
190 | )
191 | assertTrue(testFile.exists(), "file should exist")
192 | assertTrue(testFile.canRead(), "file should be readable")
193 | assertTrue(testFile.canWrite(), "file should be writable")
194 | assertTrue(testFile.isFile(), "file should be considered a file")
195 | assertFalse(testFile.isDirectory(), "file should not be considered a directory")
196 | assertTrue(testFile.delete(), "delete file failed")
197 |
198 | assertTrue(testDir.deleteRecursively(), "error while deleting all files in dir")
199 | }
200 |
201 | @Test
202 | fun testFileLengthAndAppendings() {
203 | if (platform() == Platform.Windows) {
204 | return
205 | }
206 |
207 | val testFile = Files.createTempFile("test")
208 | val data = "testData"
209 | testFile.writeText(data)
210 |
211 | assertTrue(testFile.exists(), "file should exists")
212 | assertEquals(data, testFile.readText())
213 |
214 | val appendedData = "\nNew Text!"
215 | testFile.appendBytes(appendedData.encodeToByteArray())
216 |
217 | assertEquals(data + appendedData, testFile.readText())
218 | assertEquals((data + appendedData).length.toLong(), testFile.length())
219 |
220 | assertTrue(testFile.delete(), "delete file failed")
221 | }
222 |
223 | @Test
224 | fun testFileRealPathIfRelativeLinks__posixOnly() {
225 | if (!platform().isPosix()) {
226 | return
227 | }
228 |
229 | val symlinkPrefix = if (platform() == Platform.Macos) "/private" else ""
230 |
231 | val testDir = File("/tmp/build")
232 | val testFile = Files.createTempFile(prefix = "../test", suffix = ".txt", dir = testDir)
233 | assertEquals("$symlinkPrefix/tmp/test.txt", testFile.getAbsolutePath()) // 'cause /tmp is a symlink for /private/tmp
234 | assertTrue(testFile.delete(), "delete file failed")
235 | assertTrue(testDir.delete(), "delete test folder failed")
236 | }
237 |
238 | @Test
239 | fun testFileRealPathIfRelativeLinks__jvmOnly() {
240 | if (platform() != Platform.JVM) {
241 | return
242 | }
243 |
244 | val testDir = File("/tmp/build")
245 | val testFile = Files.createTempFile(prefix = "../test", suffix = ".txt", dir = testDir)
246 | assertEquals("/tmp/build/../test.txt", testFile.getAbsolutePath()) // lazy canonicalization in jvm
247 | assertTrue(testFile.delete(), "delete file failed")
248 | assertTrue(testDir.delete(), "delete test folder failed")
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/File.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import org.w3c.files.File as JsFile
4 | import org.w3c.files.FileReaderSync
5 |
6 | actual class File constructor(jsfile: JsFile) {
7 |
8 | internal var innerFile: JsFile = jsfile
9 | private var virtual: Boolean = false
10 |
11 | actual constructor(pathname: String) : this(JsFile(emptyArray(), pathname)) {
12 | virtual = true
13 | }
14 |
15 | actual fun getParent(): String? {
16 | return when {
17 | !exists() -> null
18 | filePathSeparator in innerFile.name -> innerFile.name
19 | .split(filePathSeparator)
20 | .let { if (it.size > 1) it[it.lastIndex - 1] else it.last() }
21 | else -> innerFile.name
22 | }
23 | }
24 |
25 | actual fun getParentFile(): File? {
26 | return getParent()?.run(::File)
27 | }
28 |
29 | actual fun getName(): String {
30 | return innerFile.name
31 | }
32 |
33 | actual fun lastModified(): Long {
34 | return innerFile.lastModified.toLong()
35 | }
36 |
37 | actual fun mkdir(): Boolean {
38 | throw UnsupportedOperationException("Not available in JS!")
39 | }
40 |
41 | actual fun mkdirs(): Boolean {
42 | throw UnsupportedOperationException("Not available in JS!")
43 | }
44 |
45 | actual fun createNewFile(): Boolean {
46 | throw UnsupportedOperationException("Not available in JS!")
47 | }
48 |
49 | actual fun isFile(): Boolean {
50 | return true // always a file in js
51 | }
52 |
53 | actual fun isDirectory(): Boolean {
54 | return false
55 | }
56 |
57 | actual fun getPath(): String {
58 | return innerFile.name
59 | }
60 |
61 | actual fun getAbsolutePath(): String {
62 | return innerFile.name
63 | }
64 |
65 | actual fun length(): Long {
66 | return innerFile.size.toLong()
67 | }
68 |
69 | actual fun exists(): Boolean {
70 | return !virtual
71 | }
72 |
73 | actual fun canRead(): Boolean {
74 | return true
75 | }
76 |
77 | actual fun canWrite(): Boolean {
78 | return virtual && !innerFile.isClosed
79 | }
80 |
81 | actual fun list(): Array {
82 | throw UnsupportedOperationException("Not available in JS!")
83 | }
84 |
85 | actual fun listFiles(): Array {
86 | throw UnsupportedOperationException("Not available in JS!")
87 | }
88 |
89 | actual fun delete(): Boolean {
90 | throw UnsupportedOperationException("Not available in JS!")
91 | }
92 | }
93 |
94 | actual val filePathSeparator by lazy { '/' }
95 |
96 | actual val File.mimeType: String
97 | get() = innerFile.type
98 |
99 | actual fun File.readBytes(): ByteArray {
100 | return readText().encodeToByteArray()
101 | }
102 |
103 | actual fun File.readText(): String {
104 | return FileReaderSync().readAsText(innerFile)
105 | }
106 |
107 | actual fun File.appendText(text: String) {
108 | val newData = readText() + text
109 | writeText(newData)
110 | }
111 |
112 | actual fun File.appendBytes(bytes: ByteArray) {
113 | val newData = readBytes() + bytes
114 | writeBytes(newData)
115 | }
116 |
117 | actual fun File.writeText(text: String) {
118 | innerFile = JsFile(text.encodeToByteArray().toTypedArray(), innerFile.name)
119 | }
120 |
121 | actual fun File.writeBytes(bytes: ByteArray) {
122 | innerFile = JsFile(bytes.toTypedArray(), innerFile.name)
123 | }
124 |
--------------------------------------------------------------------------------
/src/jsMain/kotlin/platform.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | actual fun platform(): Platform = Platform.JS
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/File.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 | package me.archinamon.fileio
3 |
4 | import java.io.File as jvmFile
5 | import java.net.URLConnection
6 | import java.nio.charset.Charset
7 | import kotlin.io.appendBytes as kAppendBytes
8 | import kotlin.io.readBytes as kReadBytes
9 | import kotlin.io.writeBytes as kWriteBytes
10 |
11 | actual typealias File = jvmFile
12 |
13 | actual val filePathSeparator by lazy { File.separatorChar }
14 |
15 | actual val File.mimeType: String
16 | get() = URLConnection.guessContentTypeFromName(name)
17 |
18 | actual fun File.readBytes() = kReadBytes()
19 |
20 | actual fun File.readText() = readText(Charset.defaultCharset())
21 |
22 | actual fun File.writeBytes(bytes: ByteArray) = kWriteBytes(bytes)
23 |
24 | actual fun File.appendBytes(bytes: ByteArray) = kAppendBytes(bytes)
25 |
26 | actual fun File.appendText(text: String) = appendText(text, Charset.defaultCharset())
27 |
28 | actual fun File.writeText(text: String) = writeText(text, Charset.defaultCharset())
29 |
--------------------------------------------------------------------------------
/src/jvmMain/kotlin/platform_common.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | actual fun platform(): Platform = Platform.JVM
--------------------------------------------------------------------------------
/src/linuxX64Main/kotlin/modified.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlinx.cinterop.CPointed
4 | import kotlinx.cinterop.CPointer
5 | import kotlinx.cinterop.alloc
6 | import kotlinx.cinterop.convert
7 | import kotlinx.cinterop.memScoped
8 | import kotlinx.cinterop.ptr
9 | import platform.posix.DIR
10 | import platform.posix.dirent
11 | import platform.posix.stat
12 |
13 | internal actual fun stat.checkFileIs(flag: Int): Boolean = (st_mode.convert() and flag) == flag
14 |
15 | internal actual fun mkdir(path: String, mode: UInt): Int = platform.posix.mkdir(path, mode)
16 |
17 | internal actual fun opendir(path: String): CPointer? = platform.posix.opendir(path)
18 |
19 | @Suppress("UNCHECKED_CAST")
20 | internal actual fun readdir(dir: CPointer): CPointer? = platform.posix.readdir(dir as CPointer)
21 |
22 | @Suppress("UNCHECKED_CAST")
23 | internal actual fun closedir(dir: CPointer): Int = platform.posix.closedir(dir as CPointer)
24 |
25 | internal actual fun modified(file: File): Long = memScoped {
26 | val result = alloc()
27 | if (stat(file.getAbsolutePath(), result.ptr) != 0) {
28 | return 0L
29 | }
30 |
31 | result.st_mtim.tv_sec
32 | }
--------------------------------------------------------------------------------
/src/linuxX64Main/kotlin/platform.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | actual fun platform(): Platform = Platform.Linux
--------------------------------------------------------------------------------
/src/macosArm64Main/kotlin/modified.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlinx.cinterop.CPointed
4 | import kotlinx.cinterop.CPointer
5 | import kotlinx.cinterop.alloc
6 | import kotlinx.cinterop.convert
7 | import kotlinx.cinterop.memScoped
8 | import kotlinx.cinterop.ptr
9 | import platform.posix.DIR
10 | import platform.posix.dirent
11 | import platform.posix.stat
12 |
13 | internal actual fun stat.checkFileIs(flag: Int): Boolean = (st_mode.convert() and flag) == flag
14 |
15 | internal actual fun mkdir(path: String, mode: UInt): Int = platform.posix.mkdir(path, mode.convert())
16 |
17 | internal actual fun opendir(path: String): CPointer? = platform.posix.opendir(path)
18 |
19 | @Suppress("UNCHECKED_CAST")
20 | internal actual fun readdir(dir: CPointer): CPointer? = platform.posix.readdir(dir as CPointer)
21 |
22 | @Suppress("UNCHECKED_CAST")
23 | internal actual fun closedir(dir: CPointer): Int = platform.posix.closedir(dir as CPointer)
24 |
25 | internal actual fun modified(file: File): Long = memScoped {
26 | val result = alloc()
27 | if (stat(file.getAbsolutePath(), result.ptr) != 0) {
28 | return 0L
29 | }
30 |
31 | result.st_mtimespec.tv_sec
32 | }
--------------------------------------------------------------------------------
/src/macosArm64Main/kotlin/platform.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | actual fun platform(): Platform = Platform.Macos
--------------------------------------------------------------------------------
/src/macosRunner/kotlin/runner.kt:
--------------------------------------------------------------------------------
1 | import me.archinamon.fileio.File
2 | import me.archinamon.fileio.deleteRecursively
3 | import platform.posix.sleep
4 |
5 | fun main(args: Array) = args.forEach { path ->
6 | val file = File(path)
7 |
8 | if (path.endsWith("/")) {
9 | println("make directories... ${file.mkdirs()}")
10 | } else {
11 | println("make directories... ${file.getParentFile()?.mkdirs()}")
12 | println("creating file... ${file.createNewFile()}")
13 | }
14 |
15 | println(file.toString())
16 |
17 | sleep(5U)
18 | }.also {
19 | val file = File(args.first())
20 | print("deleting ${file.getName()}... ${file.deleteRecursively()}")
21 | }
22 |
--------------------------------------------------------------------------------
/src/macosX64Main/kotlin/modified.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlinx.cinterop.CPointed
4 | import kotlinx.cinterop.CPointer
5 | import kotlinx.cinterop.alloc
6 | import kotlinx.cinterop.convert
7 | import kotlinx.cinterop.memScoped
8 | import kotlinx.cinterop.ptr
9 | import platform.posix.DIR
10 | import platform.posix.dirent
11 | import platform.posix.stat
12 |
13 | internal actual fun stat.checkFileIs(flag: Int): Boolean = (st_mode.convert() and flag) == flag
14 |
15 | internal actual fun mkdir(path: String, mode: UInt): Int = platform.posix.mkdir(path, mode.convert())
16 |
17 | internal actual fun opendir(path: String): CPointer? = platform.posix.opendir(path)
18 |
19 | @Suppress("UNCHECKED_CAST")
20 | internal actual fun readdir(dir: CPointer): CPointer? = platform.posix.readdir(dir as CPointer)
21 |
22 | @Suppress("UNCHECKED_CAST")
23 | internal actual fun closedir(dir: CPointer): Int = platform.posix.closedir(dir as CPointer)
24 |
25 | internal actual fun modified(file: File): Long = memScoped {
26 | val result = alloc()
27 | if (stat(file.getAbsolutePath(), result.ptr) != 0) {
28 | return 0L
29 | }
30 |
31 | result.st_mtimespec.tv_sec
32 | }
--------------------------------------------------------------------------------
/src/macosX64Main/kotlin/platform.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | actual fun platform(): Platform = Platform.Macos
--------------------------------------------------------------------------------
/src/mingwX64Main/kotlin/File.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlinx.cinterop.*
4 | import platform.windows.*
5 |
6 | // Difference between January 1, 1601 and January 1, 1970 in millis
7 | private const val EPOCH_DIFF = 11644473600000
8 | private const val ERR_SIZE = -1L
9 |
10 | actual class File actual constructor(pathname: String) {
11 |
12 | private val pathname: String = pathname.replace('/', filePathSeparator)
13 |
14 | actual fun getParent(): String? {
15 | return if (exists()) getAbsolutePath().substringBeforeLast(filePathSeparator) else null
16 | }
17 |
18 | actual fun getParentFile(): File? {
19 | return getParent()?.run(::File)
20 | }
21 |
22 | actual fun getName(): String {
23 | return if (filePathSeparator in pathname) {
24 | pathname.split(filePathSeparator).last(String::isNotBlank)
25 | } else {
26 | pathname
27 | }
28 | }
29 |
30 | actual fun lastModified(): Long {
31 | val handle = CreateFileA(
32 | pathname,
33 | GENERIC_READ,
34 | 0,
35 | null,
36 | OPEN_EXISTING,
37 | FILE_ATTRIBUTE_NORMAL,
38 | null
39 | )
40 | if (handle == INVALID_HANDLE_VALUE) {
41 | return 0L
42 | }
43 |
44 | return try {
45 | memScoped {
46 | val ft = alloc<_FILETIME>()
47 | val result = GetFileTime(handle, null, null, ft.ptr)
48 | if (result == TRUE) {
49 | val st = alloc<_SYSTEMTIME>()
50 | val convertResult = FileTimeToSystemTime(ft.ptr, st.ptr)
51 | if (convertResult == TRUE) {
52 | val time = (ft.dwHighDateTime.toLong() shl 32) or ft.dwLowDateTime.toLong()
53 | (time / 10000) - EPOCH_DIFF
54 | } else 0L
55 | } else 0L
56 | }
57 | } finally {
58 | CloseHandle(handle)
59 | }
60 | }
61 |
62 | actual fun mkdir(): Boolean {
63 | if (getParentFile()?.exists() != true) {
64 | return false
65 | }
66 |
67 | if (getParentFile()?.canWrite() != true) {
68 | throw IllegalFileAccess(getAbsolutePath(), "Directory not accessible for write operations")
69 | }
70 |
71 | return SHCreateDirectoryExA(null, getAbsolutePath(), null) == ERROR_SUCCESS
72 | }
73 |
74 | actual fun mkdirs(): Boolean {
75 | if (exists()) {
76 | return false
77 | }
78 |
79 | if (mkdir()) {
80 | return true
81 | }
82 |
83 | return (getParentFile()?.mkdirs() == true || getParentFile()?.exists() == true) && mkdir()
84 | }
85 |
86 | actual fun createNewFile(): Boolean {
87 | val handle = CreateFileA(
88 | pathname,
89 | GENERIC_WRITE,
90 | FILE_SHARE_READ,
91 | null,
92 | CREATE_NEW,
93 | FILE_ATTRIBUTE_NORMAL,
94 | null
95 | )
96 |
97 | return try {
98 | handle != INVALID_HANDLE_VALUE
99 | } finally {
100 | CloseHandle(handle)
101 | }
102 | }
103 |
104 | actual fun isFile(): Boolean {
105 | return GetFileAttributesA(pathname).let { attrs ->
106 | attrs != INVALID_FILE_ATTRIBUTES &&
107 | attrs and FILE_ATTRIBUTE_DIRECTORY.toUInt() == 0u
108 | }
109 | }
110 |
111 | actual fun isDirectory(): Boolean {
112 | return GetFileAttributesA(pathname).let { attrs ->
113 | attrs != INVALID_FILE_ATTRIBUTES &&
114 | (attrs and FILE_ATTRIBUTE_DIRECTORY.toUInt() != 0u)
115 | }
116 | }
117 |
118 | actual fun getPath(): String {
119 | return pathname
120 | }
121 |
122 | actual fun getAbsolutePath(): String {
123 | return if (pathname.startsWith(filePathSeparator) || pathname.getOrNull(1) == ':') {
124 | pathname
125 | } else {
126 | memScoped {
127 | val bufLength = 200
128 | val buf = allocArray(bufLength)
129 | val result = GetCurrentDirectoryA(bufLength.toUInt(), buf).toInt()
130 | check(result != 0)
131 | if (result > bufLength) {
132 | val retryBuf = allocArray(result)
133 | GetCurrentDirectoryA(result.toUInt(), buf)
134 | check(result != 0)
135 | retryBuf.toKString()
136 | } else {
137 | buf.toKString()
138 | } + filePathSeparator + pathname
139 | }
140 | }
141 | }
142 |
143 | actual fun length(): Long = memScoped {
144 | val handle = CreateFileA(
145 | getAbsolutePath(),
146 | GENERIC_READ,
147 | FILE_SHARE_READ,
148 | null,
149 | OPEN_EXISTING,
150 | 0,
151 | null
152 | )
153 | if (handle == INVALID_HANDLE_VALUE) return ERR_SIZE
154 |
155 | return try {
156 | val fs = alloc<_LARGE_INTEGER>()
157 | if (GetFileSizeEx(handle, fs.ptr) == TRUE) {
158 | val size = (fs.HighPart.toUInt() shl 32) or fs.LowPart
159 |
160 | size.toLong()
161 | } else {
162 | ERR_SIZE
163 | }
164 | } finally {
165 | CloseHandle(handle)
166 | }
167 | }
168 |
169 | actual fun exists(): Boolean {
170 | return GetFileAttributesA(pathname) != INVALID_FILE_ATTRIBUTES
171 | }
172 |
173 | actual fun canRead(): Boolean {
174 | val handle = CreateFileA(
175 | pathname,
176 | GENERIC_READ,
177 | FILE_SHARE_READ,
178 | null,
179 | OPEN_EXISTING,
180 | 0,
181 | null
182 | )
183 |
184 | return try {
185 | handle != INVALID_HANDLE_VALUE
186 | } finally {
187 | CloseHandle(handle)
188 | }
189 | }
190 |
191 | actual fun canWrite(): Boolean {
192 | val handle = CreateFileA(
193 | pathname,
194 | GENERIC_WRITE,
195 | FILE_SHARE_WRITE,
196 | null,
197 | OPEN_EXISTING,
198 | 0,
199 | null
200 | )
201 |
202 | return try {
203 | handle != INVALID_HANDLE_VALUE
204 | } finally {
205 | CloseHandle(handle)
206 | }
207 | }
208 |
209 | actual fun list(): Array = memScoped {
210 | if (isFile()) return emptyArray()
211 |
212 | val findData = alloc()
213 | val searchPath = if (pathname.endsWith(filePathSeparator)) {
214 | pathname
215 | } else {
216 | "$pathname${filePathSeparator}"
217 | } + "*"
218 | val find = FindFirstFileA(searchPath, findData.ptr)
219 | if (find == INVALID_HANDLE_VALUE) {
220 | return emptyArray()
221 | }
222 |
223 | val files = mutableListOf()
224 | try {
225 | while (FindNextFileA(find, findData.ptr) != 0) {
226 | val fileName = findData.cFileName.toKString()
227 | if (fileName != "..") {
228 | files.add(fileName)
229 | }
230 | }
231 | } finally {
232 | FindClose(find)
233 | }
234 |
235 | return files.toTypedArray()
236 | }
237 |
238 | actual fun listFiles(): Array {
239 | if (isFile()) return emptyArray()
240 | val thisPath = getAbsolutePath().let { path ->
241 | if (!path.endsWith(filePathSeparator)) {
242 | path + filePathSeparator
243 | } else path
244 | }
245 | return list()
246 | .map { name -> File(thisPath + name) }
247 | .toTypedArray()
248 | }
249 |
250 | actual fun delete(): Boolean = memScoped {
251 | if (isFile()) return DeleteFileA(pathname) == TRUE
252 |
253 | val fileOp = alloc {
254 | hwnd = null
255 | wFunc = FO_DELETE.toUInt()
256 | pFrom = pathname.cstr.ptr
257 | pTo = null
258 | fFlags = (FOF_SILENT or FOF_NOCONFIRMATION or FOF_NOERRORUI).toUShort()
259 | fAnyOperationsAborted = FALSE
260 | hNameMappings = null
261 | lpszProgressTitle = null
262 | }
263 |
264 | return SHFileOperationA(fileOp.ptr) == 0
265 | }
266 |
267 | internal fun writeBytes(bytes: ByteArray, access: Int) {
268 | val handle = CreateFileA(
269 | getAbsolutePath(),
270 | access.toUInt(),
271 | FILE_SHARE_WRITE,
272 | null,
273 | OPEN_EXISTING,
274 | 0,
275 | null
276 | )
277 | if (handle == INVALID_HANDLE_VALUE) return
278 |
279 | try {
280 | memScoped {
281 | val bytesWritten = alloc()
282 | bytes.usePinned { b ->
283 | WriteFile(
284 | handle,
285 | b.addressOf(0),
286 | bytes.size.toUInt(),
287 | bytesWritten.ptr,
288 | null
289 | )
290 | }
291 | }
292 | } finally {
293 | CloseHandle(handle)
294 | }
295 | }
296 |
297 | override fun toString(): String {
298 | return "File {\n" +
299 | "path=${getAbsolutePath()}\n" +
300 | "name=${getName()}\n" +
301 | "exists=${exists()}\n" +
302 | "canRead=${canRead()}\n" +
303 | "canWrite=${canWrite()}\n" +
304 | "isFile=${isFile()}\n" +
305 | "isDirectory=${isDirectory()}\n" +
306 | "lastModified=${lastModified()}\n" +
307 | (if (isDirectory()) "files=[${listFiles().joinToString()}]" else "") +
308 | "}"
309 | }
310 | }
311 |
312 | actual val filePathSeparator by lazy { if (platform() == Platform.Windows) '\\' else '/' }
313 |
314 | actual val File.mimeType: String
315 | get() = ""
316 |
317 | actual fun File.readBytes(): ByteArray = memScoped {
318 | val handle = CreateFileA(
319 | getAbsolutePath(),
320 | GENERIC_READ,
321 | FILE_SHARE_READ,
322 | null,
323 | OPEN_EXISTING,
324 | 0,
325 | null
326 | )
327 | if (handle == INVALID_HANDLE_VALUE) return byteArrayOf()
328 |
329 | try {
330 | val fs = alloc<_LARGE_INTEGER>()
331 | if (GetFileSizeEx(handle, fs.ptr) == TRUE) {
332 | val size = (fs.HighPart.toUInt() shl 32) or fs.LowPart
333 | val buf = allocArray(size.toInt())
334 | val bytesRead = alloc()
335 | if (ReadFile(handle, buf, size, bytesRead.ptr, null) == TRUE) {
336 | buf.readBytes(bytesRead.value.toInt())
337 | } else {
338 | byteArrayOf()
339 | }
340 | } else {
341 | byteArrayOf()
342 | }
343 | } finally {
344 | CloseHandle(handle)
345 | }
346 | }
347 |
348 | actual fun File.writeBytes(bytes: ByteArray) {
349 | // no need to use pinning or memscope, cause it's inside the method already does
350 | writeBytes(bytes, GENERIC_WRITE)
351 | }
352 |
353 | actual fun File.readText(): String {
354 | return readBytes().toKString()
355 | }
356 |
357 | actual fun File.appendText(text: String) {
358 | writeBytes(text.encodeToByteArray(), FILE_APPEND_DATA)
359 | }
360 |
361 | actual fun File.appendBytes(bytes: ByteArray) {
362 | writeBytes(bytes, FILE_APPEND_DATA)
363 | }
364 |
365 | actual fun File.writeText(text: String) {
366 | writeBytes(text.encodeToByteArray(), GENERIC_WRITE)
367 | }
368 |
--------------------------------------------------------------------------------
/src/mingwX64Main/kotlin/platform.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | actual fun platform(): Platform = Platform.Windows
--------------------------------------------------------------------------------
/src/posixMain/kotlin/File.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlinx.cinterop.CPointed
4 | import kotlinx.cinterop.CPointer
5 | import kotlinx.cinterop.addressOf
6 | import kotlinx.cinterop.alloc
7 | import kotlinx.cinterop.allocArray
8 | import kotlinx.cinterop.convert
9 | import kotlinx.cinterop.memScoped
10 | import kotlinx.cinterop.pointed
11 | import kotlinx.cinterop.ptr
12 | import kotlinx.cinterop.refTo
13 | import kotlinx.cinterop.toKString
14 | import kotlinx.cinterop.usePinned
15 | import platform.posix.FILENAME_MAX
16 | import platform.posix.F_OK
17 | import platform.posix.NULL
18 | import platform.posix.O_APPEND
19 | import platform.posix.O_CREAT
20 | import platform.posix.O_RDWR
21 | import platform.posix.PATH_MAX
22 | import platform.posix.R_OK
23 | import platform.posix.SEEK_END
24 | import platform.posix.SEEK_SET
25 | import platform.posix.S_IFDIR
26 | import platform.posix.S_IFREG
27 | import platform.posix.S_IRWXG
28 | import platform.posix.S_IRWXO
29 | import platform.posix.S_IRWXU
30 | import platform.posix.W_OK
31 | import platform.posix.access
32 | import platform.posix.dirent
33 | import platform.posix.fclose
34 | import platform.posix.fopen
35 | import platform.posix.fread
36 | import platform.posix.fseek
37 | import platform.posix.ftell
38 | import platform.posix.fwrite
39 | import platform.posix.getcwd
40 | import platform.posix.realpath
41 | import platform.posix.rmdir
42 | import platform.posix.stat
43 | import platform.posix.strlen
44 | import platform.posix.unlink
45 |
46 | actual class File actual constructor(
47 | private val pathname: String
48 | ) {
49 |
50 | internal val modeRead = "r"
51 | private val modeAppend = "a"
52 | private val modeRewrite = "w"
53 |
54 | private val absPath: String
55 |
56 | init {
57 | // not lazy but preserve on obj init
58 | absPath = getRealAbsolutePath()
59 | }
60 |
61 | actual fun getParent(): String? {
62 | val path = getAbsolutePath()
63 | val idx = path.lastIndexOf(filePathSeparator)
64 | if (idx <= 0 || idx > path.length) {
65 | return null
66 | }
67 |
68 | return path.substringBeforeLast(filePathSeparator)
69 | }
70 |
71 | actual fun getParentFile(): File? {
72 | return getParent()?.run(::File)
73 | }
74 |
75 | actual fun getName(): String {
76 | return if (filePathSeparator in pathname) {
77 | pathname.split(filePathSeparator).last(String::isNotBlank)
78 | } else {
79 | pathname
80 | }
81 | }
82 |
83 | actual fun getPath(): String = pathname
84 |
85 | actual fun getAbsolutePath(): String {
86 | if (absPath.isNotBlank()) {
87 | return absPath
88 | }
89 |
90 | return getRealAbsolutePath()
91 | }
92 |
93 | private fun getRealAbsolutePath(): String {
94 | return memScoped {
95 | val path = if (pathname.first() != filePathSeparator || pathname.first() == '.') {
96 | getcwd(allocArray(FILENAME_MAX), FILENAME_MAX.convert())
97 | ?.toKString() + filePathSeparator + pathname
98 | } else {
99 | pathname
100 | }
101 |
102 | if (".." !in path && "./" !in path) {
103 | return path
104 | }
105 |
106 | ByteArray(PATH_MAX).usePinned { absolutePath ->
107 | realpath(path, absolutePath.addressOf(0))
108 |
109 | absolutePath.get().toKString()
110 | }
111 | }
112 | }
113 |
114 | actual fun length(): Long {
115 | memScoped {
116 | val result = alloc()
117 |
118 | stat(pathname, result.ptr)
119 | .ensureUnixCallResult("stat") { ret -> ret == 0 }
120 |
121 | return result.st_size
122 | }
123 | }
124 |
125 | actual fun lastModified(): Long = modified(this)
126 |
127 | actual fun mkdir(): Boolean {
128 | if (getParentFile()?.exists() != true) {
129 | return false
130 | }
131 |
132 | if (getParentFile()?.canWrite() != true) {
133 | throw IllegalFileAccess(pathname, "Directory not accessible for write operations")
134 | }
135 |
136 | mkdir(pathname, (S_IRWXU or S_IRWXG or S_IRWXO).convert())
137 | .ensureUnixCallResult("mkdir") { ret -> ret == 0 }
138 |
139 | return exists()
140 | }
141 |
142 | actual fun mkdirs(): Boolean {
143 | if (exists()) {
144 | return false
145 | }
146 |
147 | if (mkdir()) {
148 | return true
149 | }
150 |
151 | return (getParentFile()?.mkdirs() == true || getParentFile()?.exists() == true) && mkdir()
152 | }
153 |
154 | actual fun createNewFile(): Boolean {
155 | if (exists()) {
156 | return true
157 | }
158 |
159 | fopen(pathname, modeRewrite).let { fd ->
160 | fclose(fd).ensureUnixCallResult("fclose") { ret -> ret == 0 }
161 | }
162 |
163 | return exists()
164 | }
165 |
166 | actual fun isFile(): Boolean {
167 | if (!exists()) {
168 | return false
169 | }
170 |
171 | return memScoped {
172 | val result = alloc()
173 |
174 | stat(pathname, result.ptr)
175 | .ensureUnixCallResult("stat") { ret -> ret == 0 }
176 |
177 | return@memScoped result.checkFileIs(S_IFREG)
178 | }
179 | }
180 |
181 | actual fun isDirectory(): Boolean {
182 | if (!exists()) {
183 | return false
184 | }
185 |
186 | return memScoped {
187 | val result = alloc()
188 |
189 | stat(pathname, result.ptr)
190 | .ensureUnixCallResult("stat") { ret -> ret == 0 }
191 |
192 | return@memScoped result.checkFileIs(S_IFDIR)
193 | }
194 | }
195 |
196 | actual fun list(): Array = memScoped {
197 | val dir = opendir(pathname)
198 | ?: return emptyArray()
199 |
200 | val result = ArrayList()
201 |
202 | do {
203 | val record = readdir(dir)
204 |
205 | record?.pointed?.let { entity: dirent ->
206 | result.add(entity.d_name.toKString())
207 | }
208 | } while (record != NULL)
209 |
210 | closedir(dir).ensureUnixCallResult("closedir") { ret -> ret == 0 }
211 |
212 | return result.filter { name -> name !in arrayOf(".", "..") }
213 | .toTypedArray()
214 | }
215 |
216 | actual fun listFiles(): Array {
217 | val thisPath = getAbsolutePath().let { path ->
218 | if (!path.endsWith(filePathSeparator)) {
219 | path + filePathSeparator
220 | } else path
221 | }
222 | return list()
223 | .map { name -> File(thisPath + name) }
224 | .toTypedArray()
225 | }
226 |
227 | actual fun delete(): Boolean {
228 | if (isDirectory()) {
229 | return rmdir(pathname) == 0 // do not throw errors here
230 | }
231 |
232 | return unlink(pathname) == 0
233 | }
234 |
235 | actual fun exists(): Boolean {
236 | return access(pathname, F_OK) != -1
237 | }
238 |
239 | actual fun canRead(): Boolean {
240 | return access(pathname, R_OK) != -1
241 | }
242 |
243 | actual fun canWrite(): Boolean {
244 | return access(pathname, W_OK) != -1
245 | }
246 |
247 | internal fun writeBytes(bytes: ByteArray, mode: Int, size: ULong = ULong.MAX_VALUE, elemSize: ULong = 1U) {
248 | if (!exists()) {
249 | throw NoSuchElementException("File or directory '${getAbsolutePath()}' not exists")
250 | }
251 |
252 | val fd = fopen(getAbsolutePath(), if (mode and O_APPEND == O_APPEND) modeAppend else modeRewrite)
253 | try {
254 | memScoped {
255 | bytes.usePinned { pinnedBytes ->
256 | val bytesSize: ULong = if (size != ULong.MAX_VALUE) size else pinnedBytes.get().size.convert()
257 | fwrite(pinnedBytes.addressOf(0), elemSize, bytesSize, fd)
258 | .ensureUnixCallResult("fwrite") { ret -> ret == bytesSize }
259 | }
260 | }
261 | } finally {
262 | fclose(fd).ensureUnixCallResult("fclose") { ret -> ret == 0 }
263 | }
264 | }
265 |
266 | override fun toString(): String {
267 | return "File {\n" +
268 | "path=${getAbsolutePath()}\n" +
269 | "name=${getName()}\n" +
270 | "exists=${exists()}\n" +
271 | "canRead=${canRead()}\n" +
272 | "canWrite=${canWrite()}\n" +
273 | "isFile=${isFile()}\n" +
274 | "isDirectory=${isDirectory()}\n" +
275 | "lastModified=${lastModified()}\n" +
276 | (if (isDirectory()) "files=[${listFiles().joinToString()}]" else "") +
277 | "}"
278 | }
279 | }
280 |
281 | internal expect fun modified(file: File): Long
282 |
283 | internal expect fun stat.checkFileIs(flag: Int): Boolean
284 |
285 | internal expect fun mkdir(path: String, mode: UInt): Int
286 |
287 | internal expect fun opendir(path: String): CPointer?
288 |
289 | internal expect fun readdir(dir: CPointer): CPointer?
290 |
291 | internal expect fun closedir(dir: CPointer): Int
292 |
293 | @SharedImmutable
294 | actual val filePathSeparator by lazy { if (platform() == Platform.Windows) '\\' else '/' }
295 |
296 | //todo determine mimeType on file extension; see jdk mappings
297 | actual val File.mimeType: String
298 | get() = ""
299 |
300 | actual fun File.readBytes(): ByteArray {
301 | if (!exists()) {
302 | throw NoSuchElementException("File or directory '${getAbsolutePath()}' not exists")
303 | }
304 |
305 | if (length() == 0L) {
306 | return byteArrayOf(0x0)
307 | }
308 |
309 | val fd = fopen(getAbsolutePath(), modeRead)
310 | try {
311 | memScoped {
312 | fseek(fd, 0, SEEK_END)
313 | val size = ftell(fd).convert()
314 | fseek(fd, 0, SEEK_SET)
315 |
316 | return ByteArray(size).also { buffer ->
317 | fread(buffer.refTo(0), 1UL, size.convert(), fd)
318 | .ensureUnixCallResult("fread") { ret -> ret > 0U }
319 | }
320 | }
321 | } finally {
322 | fclose(fd).ensureUnixCallResult("fclose") { ret -> ret == 0 }
323 | }
324 | }
325 |
326 | actual fun File.writeBytes(bytes: ByteArray) {
327 | // no need to use pinning or memscope, 'cause it's inside the method already does
328 | writeBytes(bytes, O_RDWR, bytes.size.convert(), Byte.SIZE_BYTES.convert())
329 | }
330 |
331 | actual fun File.readText(): String {
332 | return readBytes().toKString()
333 | }
334 |
335 | actual fun File.appendText(text: String) {
336 | writeBytes(text.encodeToByteArray(), O_RDWR or O_APPEND, strlen(text))
337 | }
338 |
339 | actual fun File.appendBytes(bytes: ByteArray) {
340 | writeBytes(bytes, O_RDWR or O_APPEND, bytes.size.convert())
341 | }
342 |
343 | actual fun File.writeText(text: String) {
344 | writeBytes(text.encodeToByteArray(), O_RDWR or O_CREAT, strlen(text))
345 | }
346 |
--------------------------------------------------------------------------------
/src/posixMain/kotlin/posix.kt:
--------------------------------------------------------------------------------
1 | package me.archinamon.fileio
2 |
3 | import kotlinx.cinterop.toKString
4 | import platform.posix.posix_errno
5 | import platform.posix.strerror
6 |
7 | inline fun Int.ensureUnixCallResult(op: String, predicate: (Int) -> Boolean): Int {
8 | if (!predicate(this)) {
9 | throw Error("$op: ${strerror(posix_errno())!!.toKString()}")
10 | }
11 | return this
12 | }
13 |
14 | inline fun Long.ensureUnixCallResult(op: String, predicate: (Long) -> Boolean): Long {
15 | if (!predicate(this)) {
16 | throw Error("$op: ${strerror(posix_errno())!!.toKString()}")
17 | }
18 | return this
19 | }
20 |
21 | inline fun ULong.ensureUnixCallResult(op: String, predicate: (ULong) -> Boolean): ULong {
22 | if (!predicate(this)) {
23 | throw Error("$op: ${strerror(posix_errno())!!.toKString()}")
24 | }
25 | return this
26 | }
27 |
--------------------------------------------------------------------------------