magicicapsula 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- magicicapsula-0.1.0/LICENSE +21 -0
- magicicapsula-0.1.0/PKG-INFO +169 -0
- magicicapsula-0.1.0/README.md +141 -0
- magicicapsula-0.1.0/magicicapsula/__init__.py +1 -0
- magicicapsula-0.1.0/magicicapsula/__main__.py +3 -0
- magicicapsula-0.1.0/magicicapsula/assets/logo.txt +27 -0
- magicicapsula-0.1.0/magicicapsula/cli.py +38 -0
- magicicapsula-0.1.0/magicicapsula/commands/__init__.py +0 -0
- magicicapsula-0.1.0/magicicapsula/commands/_style.py +59 -0
- magicicapsula-0.1.0/magicicapsula/commands/_util.py +26 -0
- magicicapsula-0.1.0/magicicapsula/commands/add.py +18 -0
- magicicapsula-0.1.0/magicicapsula/commands/info.py +27 -0
- magicicapsula-0.1.0/magicicapsula/commands/init.py +31 -0
- magicicapsula-0.1.0/magicicapsula/commands/open.py +29 -0
- magicicapsula-0.1.0/magicicapsula/commands/rm.py +17 -0
- magicicapsula-0.1.0/magicicapsula/commands/seal.py +66 -0
- magicicapsula-0.1.0/magicicapsula/commands/status.py +37 -0
- magicicapsula-0.1.0/magicicapsula/commands/verify.py +18 -0
- magicicapsula-0.1.0/magicicapsula/commands/version.py +14 -0
- magicicapsula-0.1.0/magicicapsula/core/__init__.py +5 -0
- magicicapsula-0.1.0/magicicapsula/core/capsule.py +165 -0
- magicicapsula-0.1.0/magicicapsula/core/crypto.py +69 -0
- magicicapsula-0.1.0/magicicapsula/core/draft.py +108 -0
- magicicapsula-0.1.0/magicicapsula/core/errors.py +29 -0
- magicicapsula-0.1.0/magicicapsula.egg-info/PKG-INFO +169 -0
- magicicapsula-0.1.0/magicicapsula.egg-info/SOURCES.txt +30 -0
- magicicapsula-0.1.0/magicicapsula.egg-info/dependency_links.txt +1 -0
- magicicapsula-0.1.0/magicicapsula.egg-info/entry_points.txt +2 -0
- magicicapsula-0.1.0/magicicapsula.egg-info/requires.txt +1 -0
- magicicapsula-0.1.0/magicicapsula.egg-info/top_level.txt +1 -0
- magicicapsula-0.1.0/pyproject.toml +46 -0
- magicicapsula-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iDavi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: magicicapsula
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: seal files now, open them later
|
|
5
|
+
Author-email: iDavi <odavi20527@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iDavi/magicicapsula
|
|
8
|
+
Project-URL: Repository, https://github.com/iDavi/magicicapsula
|
|
9
|
+
Project-URL: Issues, https://github.com/iDavi/magicicapsula/issues
|
|
10
|
+
Keywords: time-capsule,encryption,cli,archive,vault
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security :: Cryptography
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: cryptography>=42
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# magicicapsula
|
|
30
|
+
|
|
31
|
+
## install
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
needs python 3.10+. one dependency: `cryptography`.
|
|
38
|
+
|
|
39
|
+
## how it works
|
|
40
|
+
|
|
41
|
+
the workflow is staged, so you don't have to add everything at once:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
magicicapsula init -u 2030-01-01 # start a draft here
|
|
45
|
+
magicicapsula add letter.txt photos/ # stage files/folders
|
|
46
|
+
magicicapsula add diary.txt # add more later
|
|
47
|
+
magicicapsula status # see what's staged
|
|
48
|
+
magicicapsula seal # pack it all into capsule.mcap
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
the draft lives in a `.capsule/` directory (found by walking up from the
|
|
52
|
+
current dir). `seal` reads everything staged and writes the `.mcap` file.
|
|
53
|
+
|
|
54
|
+
later, when the date has passed:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
magicicapsula open capsule.mcap -d ./out
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`.mcap` is one portable binary file. store it anywhere, copy it around. it
|
|
61
|
+
holds any file type (images, pdfs, binaries) byte for byte, not just text.
|
|
62
|
+
|
|
63
|
+
## passwords
|
|
64
|
+
|
|
65
|
+
- with a password (default): contents are encrypted (aes-128 via `cryptography`),
|
|
66
|
+
unreadable without the password. `open` and `verify` prompt for it.
|
|
67
|
+
- without a password (`seal --no-password`): no encryption. the unlock date is
|
|
68
|
+
the only gate, so anyone with the file can open it after that date. don't put
|
|
69
|
+
anything private in a no-password capsule.
|
|
70
|
+
|
|
71
|
+
note: the unlock date is enforced by the tool, not by cryptography. if you hold
|
|
72
|
+
the password you could decrypt early with other means. the date stops casual
|
|
73
|
+
early opening, not a determined holder.
|
|
74
|
+
|
|
75
|
+
## commands
|
|
76
|
+
|
|
77
|
+
### init
|
|
78
|
+
start a new capsule draft in the current directory.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
magicicapsula init [-u DATE] [-n NOTE] [-o OUT]
|
|
82
|
+
|
|
83
|
+
-u, --unlock DATE unlock date, can also be set at seal
|
|
84
|
+
-n, --note NOTE plaintext note shown by info
|
|
85
|
+
-o, --out OUT output file name (default: capsule.mcap)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### add
|
|
89
|
+
stage files or folders to put in the capsule.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
magicicapsula add <paths...>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### status
|
|
96
|
+
show the draft: unlock date and staged files.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
magicicapsula status
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### rm
|
|
103
|
+
unstage files. does not delete them from disk.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
magicicapsula rm <paths...>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### seal
|
|
110
|
+
seal everything staged into a capsule file. flags override the draft's
|
|
111
|
+
settings and stick.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
magicicapsula seal [-u DATE] [-o FILE] [-n NOTE] [-f] [-P]
|
|
115
|
+
|
|
116
|
+
-u, --unlock DATE unlock date, overrides the draft's
|
|
117
|
+
-o, --out FILE output capsule file, overrides the draft's
|
|
118
|
+
-n, --note NOTE plaintext note, overrides the draft's
|
|
119
|
+
-f, --force overwrite the output if it exists
|
|
120
|
+
-P, --no-password seal without a password (anyone can open it after the date)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### info
|
|
124
|
+
show a capsule's dates and status. no password needed.
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
magicicapsula info <file>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### open
|
|
131
|
+
open a capsule and extract it, once the unlock date has passed.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
magicicapsula open [-d DEST] <file>
|
|
135
|
+
|
|
136
|
+
-d, --dest DEST directory to extract into (default: current dir)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### verify
|
|
140
|
+
check a capsule's integrity (and the password, if any) without opening it.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
magicicapsula verify <file>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### version
|
|
147
|
+
show the version and logo.
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
magicicapsula version
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## date format
|
|
154
|
+
|
|
155
|
+
`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, read as local time. examples:
|
|
156
|
+
`2030-01-01`, `2030-01-01T08:00`.
|
|
157
|
+
|
|
158
|
+
## colors
|
|
159
|
+
|
|
160
|
+
output is colored in a terminal and plain when piped or redirected. set
|
|
161
|
+
`NO_COLOR=1` to turn colors off.
|
|
162
|
+
|
|
163
|
+
## dates and gotchas
|
|
164
|
+
|
|
165
|
+
- staged entries are paths, read at seal time, not copied when you add them.
|
|
166
|
+
if a staged file is moved or deleted before sealing, `status` marks it
|
|
167
|
+
`(missing)` and `seal` refuses until it's fixed.
|
|
168
|
+
- files are stored under their base name, so two staged files with the same
|
|
169
|
+
name would collide.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# magicicapsula
|
|
2
|
+
|
|
3
|
+
## install
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install -e .
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
needs python 3.10+. one dependency: `cryptography`.
|
|
10
|
+
|
|
11
|
+
## how it works
|
|
12
|
+
|
|
13
|
+
the workflow is staged, so you don't have to add everything at once:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
magicicapsula init -u 2030-01-01 # start a draft here
|
|
17
|
+
magicicapsula add letter.txt photos/ # stage files/folders
|
|
18
|
+
magicicapsula add diary.txt # add more later
|
|
19
|
+
magicicapsula status # see what's staged
|
|
20
|
+
magicicapsula seal # pack it all into capsule.mcap
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
the draft lives in a `.capsule/` directory (found by walking up from the
|
|
24
|
+
current dir). `seal` reads everything staged and writes the `.mcap` file.
|
|
25
|
+
|
|
26
|
+
later, when the date has passed:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
magicicapsula open capsule.mcap -d ./out
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`.mcap` is one portable binary file. store it anywhere, copy it around. it
|
|
33
|
+
holds any file type (images, pdfs, binaries) byte for byte, not just text.
|
|
34
|
+
|
|
35
|
+
## passwords
|
|
36
|
+
|
|
37
|
+
- with a password (default): contents are encrypted (aes-128 via `cryptography`),
|
|
38
|
+
unreadable without the password. `open` and `verify` prompt for it.
|
|
39
|
+
- without a password (`seal --no-password`): no encryption. the unlock date is
|
|
40
|
+
the only gate, so anyone with the file can open it after that date. don't put
|
|
41
|
+
anything private in a no-password capsule.
|
|
42
|
+
|
|
43
|
+
note: the unlock date is enforced by the tool, not by cryptography. if you hold
|
|
44
|
+
the password you could decrypt early with other means. the date stops casual
|
|
45
|
+
early opening, not a determined holder.
|
|
46
|
+
|
|
47
|
+
## commands
|
|
48
|
+
|
|
49
|
+
### init
|
|
50
|
+
start a new capsule draft in the current directory.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
magicicapsula init [-u DATE] [-n NOTE] [-o OUT]
|
|
54
|
+
|
|
55
|
+
-u, --unlock DATE unlock date, can also be set at seal
|
|
56
|
+
-n, --note NOTE plaintext note shown by info
|
|
57
|
+
-o, --out OUT output file name (default: capsule.mcap)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### add
|
|
61
|
+
stage files or folders to put in the capsule.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
magicicapsula add <paths...>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### status
|
|
68
|
+
show the draft: unlock date and staged files.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
magicicapsula status
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### rm
|
|
75
|
+
unstage files. does not delete them from disk.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
magicicapsula rm <paths...>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### seal
|
|
82
|
+
seal everything staged into a capsule file. flags override the draft's
|
|
83
|
+
settings and stick.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
magicicapsula seal [-u DATE] [-o FILE] [-n NOTE] [-f] [-P]
|
|
87
|
+
|
|
88
|
+
-u, --unlock DATE unlock date, overrides the draft's
|
|
89
|
+
-o, --out FILE output capsule file, overrides the draft's
|
|
90
|
+
-n, --note NOTE plaintext note, overrides the draft's
|
|
91
|
+
-f, --force overwrite the output if it exists
|
|
92
|
+
-P, --no-password seal without a password (anyone can open it after the date)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### info
|
|
96
|
+
show a capsule's dates and status. no password needed.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
magicicapsula info <file>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### open
|
|
103
|
+
open a capsule and extract it, once the unlock date has passed.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
magicicapsula open [-d DEST] <file>
|
|
107
|
+
|
|
108
|
+
-d, --dest DEST directory to extract into (default: current dir)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### verify
|
|
112
|
+
check a capsule's integrity (and the password, if any) without opening it.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
magicicapsula verify <file>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### version
|
|
119
|
+
show the version and logo.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
magicicapsula version
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## date format
|
|
126
|
+
|
|
127
|
+
`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, read as local time. examples:
|
|
128
|
+
`2030-01-01`, `2030-01-01T08:00`.
|
|
129
|
+
|
|
130
|
+
## colors
|
|
131
|
+
|
|
132
|
+
output is colored in a terminal and plain when piped or redirected. set
|
|
133
|
+
`NO_COLOR=1` to turn colors off.
|
|
134
|
+
|
|
135
|
+
## dates and gotchas
|
|
136
|
+
|
|
137
|
+
- staged entries are paths, read at seal time, not copied when you add them.
|
|
138
|
+
if a staged file is moved or deleted before sealing, `status` marks it
|
|
139
|
+
`(missing)` and `seal` refuses until it's fixed.
|
|
140
|
+
- files are stored under their base name, so two staged files with the same
|
|
141
|
+
name would collide.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m
|
|
3
|
+
[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m
|
|
4
|
+
[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m
|
|
5
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m [38;2;244;244;244m░[0m
|
|
6
|
+
[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m[38;2;244;244;244m░[0m[38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m [38;2;244;244;244m░[0m
|
|
7
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;240;0;12m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;240;0;12m▓[0m [38;2;244;244;244m░[0m
|
|
8
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m[38;2;244;244;244m░[0m[38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m [38;2;244;244;244m░[0m
|
|
9
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m[38;2;240;0;12m▓[0m [38;2;244;244;244m░[0m
|
|
10
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;244;244;244m░[0m
|
|
11
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m
|
|
12
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m
|
|
13
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m
|
|
14
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m[38;2;0;0;0m█[0m[38;2;153;133;0m▒[0m [38;2;244;244;244m░[0m
|
|
15
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m
|
|
16
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m
|
|
17
|
+
[38;2;107;75;0m▓[0m [38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m
|
|
18
|
+
[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m [38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m
|
|
19
|
+
[38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m[38;2;107;75;0m▓[0m[38;2;107;75;0m▓[0m[38;2;244;244;244m░[0m
|
|
20
|
+
[38;2;244;244;244m░[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m [38;2;244;244;244m░[0m[38;2;244;244;244m░[0m
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib
|
|
3
|
+
import pkgutil
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from magicicapsula import commands
|
|
7
|
+
from magicicapsula.commands import _style
|
|
8
|
+
from magicicapsula.core.errors import CapsuleError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="magicicapsula",
|
|
14
|
+
description="seal files now, open them later",
|
|
15
|
+
)
|
|
16
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
17
|
+
sub.required = True
|
|
18
|
+
|
|
19
|
+
# every non-underscore module in commands/ with a register() becomes a command.
|
|
20
|
+
# drop in a new file and it shows up, nothing else to wire.
|
|
21
|
+
for _, name, _ in pkgutil.iter_modules(commands.__path__):
|
|
22
|
+
if name.startswith("_"):
|
|
23
|
+
continue
|
|
24
|
+
mod = importlib.import_module(f"magicicapsula.commands.{name}")
|
|
25
|
+
if hasattr(mod, "register"):
|
|
26
|
+
mod.register(sub)
|
|
27
|
+
|
|
28
|
+
return parser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
args = build_parser().parse_args()
|
|
33
|
+
try:
|
|
34
|
+
args.func(args)
|
|
35
|
+
except CapsuleError as exc:
|
|
36
|
+
sys.exit(_style.red(f"error: {exc}"))
|
|
37
|
+
except FileNotFoundError as exc:
|
|
38
|
+
sys.exit(_style.red(f"error: no such file: {exc}"))
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""colors and the logo. presentation only, so it stays in the cli layer.
|
|
2
|
+
|
|
3
|
+
colors switch off automatically when output isn't a terminal, or when
|
|
4
|
+
NO_COLOR is set, so piped/redirected output stays clean.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from importlib import resources
|
|
11
|
+
|
|
12
|
+
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def enabled():
|
|
16
|
+
return (
|
|
17
|
+
sys.stdout.isatty()
|
|
18
|
+
and os.environ.get("NO_COLOR") is None
|
|
19
|
+
and os.environ.get("TERM") != "dumb"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def paint(text, code):
|
|
24
|
+
return f"\x1b[{code}m{text}\x1b[0m" if enabled() else text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def bold(t):
|
|
28
|
+
return paint(t, "1")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dim(t):
|
|
32
|
+
return paint(t, "2")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def red(t):
|
|
36
|
+
return paint(t, "31")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def green(t):
|
|
40
|
+
return paint(t, "32")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def yellow(t):
|
|
44
|
+
return paint(t, "33")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cyan(t):
|
|
48
|
+
return paint(t, "36")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def logo():
|
|
52
|
+
text = resources.files("magicicapsula").joinpath("assets/logo.txt").read_text(encoding="utf-8")
|
|
53
|
+
lines = text.splitlines()
|
|
54
|
+
while lines and not lines[0].strip():
|
|
55
|
+
lines.pop(0)
|
|
56
|
+
while lines and not lines[-1].strip():
|
|
57
|
+
lines.pop()
|
|
58
|
+
text = "\n".join(lines)
|
|
59
|
+
return text if enabled() else _ANSI.sub("", text)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""small helpers shared by the commands. underscore name so it isn't a command."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_capsule(path):
|
|
8
|
+
with open(path, "rb") as fh:
|
|
9
|
+
return fh.read()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ask_password(confirm=False):
|
|
13
|
+
pw = getpass.getpass("password: ")
|
|
14
|
+
if not pw:
|
|
15
|
+
raise SystemExit("error: empty password")
|
|
16
|
+
if confirm and pw != getpass.getpass("confirm password: "):
|
|
17
|
+
raise SystemExit("error: passwords do not match")
|
|
18
|
+
return pw
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fmt_remaining(delta: timedelta) -> str:
|
|
22
|
+
secs = max(int(delta.total_seconds()), 0)
|
|
23
|
+
days, secs = divmod(secs, 86400)
|
|
24
|
+
hours, secs = divmod(secs, 3600)
|
|
25
|
+
mins = secs // 60
|
|
26
|
+
return f"{days}d {hours}h {mins}m"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from magicicapsula.core import draft
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def register(sub):
|
|
5
|
+
p = sub.add_parser("add", help="stage files or folders to put in the capsule")
|
|
6
|
+
p.add_argument("paths", nargs="+", help="files or folders to stage")
|
|
7
|
+
p.set_defaults(func=run)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(args):
|
|
11
|
+
d = draft.load()
|
|
12
|
+
added = draft.add(d, args.paths)
|
|
13
|
+
if not added:
|
|
14
|
+
print("nothing new to stage")
|
|
15
|
+
return
|
|
16
|
+
for p in added:
|
|
17
|
+
print(f" staged {p}")
|
|
18
|
+
print(f"{len(d.staged)} item(s) staged in total")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from magicicapsula.core import capsule
|
|
4
|
+
from magicicapsula.commands import _style
|
|
5
|
+
from magicicapsula.commands._util import fmt_remaining, read_capsule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(sub):
|
|
9
|
+
p = sub.add_parser("info", help="show a capsule's dates and status (no password needed)")
|
|
10
|
+
p.add_argument("file", help="capsule file")
|
|
11
|
+
p.set_defaults(func=run)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(args):
|
|
15
|
+
info = capsule.inspect(read_capsule(args.file))
|
|
16
|
+
now = datetime.now(timezone.utc)
|
|
17
|
+
print(f"created: {info.created_at.astimezone().isoformat()}")
|
|
18
|
+
print(f"unlocks: {info.unlock_at.astimezone().isoformat()}")
|
|
19
|
+
print(f"cipher: {info.cipher}")
|
|
20
|
+
if info.cipher == "none":
|
|
21
|
+
print(_style.dim(" no password, opens for anyone after the unlock date"))
|
|
22
|
+
if info.note:
|
|
23
|
+
print(f"note: {info.note}")
|
|
24
|
+
if info.is_open(now):
|
|
25
|
+
print(f"status: {_style.green('open')}, the unlock date has passed")
|
|
26
|
+
else:
|
|
27
|
+
print(f"status: {_style.yellow('locked')}, {fmt_remaining(info.unlock_at - now)} remaining")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from magicicapsula.core import draft
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register(sub):
|
|
7
|
+
p = sub.add_parser("init", help="start a new capsule draft in the current directory")
|
|
8
|
+
p.add_argument("-u", "--unlock", metavar="DATE", help="unlock date, can also be set at seal")
|
|
9
|
+
p.add_argument("-n", "--note", default="", help="plaintext note shown by info")
|
|
10
|
+
p.add_argument("-o", "--out", default="capsule.mcap", help="output file name")
|
|
11
|
+
p.set_defaults(func=run)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(args):
|
|
15
|
+
try:
|
|
16
|
+
d = draft.init()
|
|
17
|
+
except FileExistsError:
|
|
18
|
+
raise SystemExit("error: a capsule draft already exists here (.capsule/)")
|
|
19
|
+
|
|
20
|
+
if args.unlock:
|
|
21
|
+
try:
|
|
22
|
+
datetime.fromisoformat(args.unlock)
|
|
23
|
+
except ValueError:
|
|
24
|
+
raise SystemExit(f"error: bad date {args.unlock!r} (use YYYY-MM-DD or YYYY-MM-DDTHH:MM)")
|
|
25
|
+
d.unlock_at = args.unlock
|
|
26
|
+
d.note = args.note
|
|
27
|
+
d.out = args.out
|
|
28
|
+
draft.save(d)
|
|
29
|
+
|
|
30
|
+
print(f"new capsule draft in {d.dir}")
|
|
31
|
+
print("next: magicicapsula add <files...>")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from magicicapsula.core import capsule
|
|
4
|
+
from magicicapsula.commands import _style
|
|
5
|
+
from magicicapsula.commands._util import ask_password, fmt_remaining, read_capsule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(sub):
|
|
9
|
+
p = sub.add_parser("open", help="open a capsule and extract it once the unlock date has passed")
|
|
10
|
+
p.add_argument("file", help="capsule file")
|
|
11
|
+
p.add_argument("-d", "--dest", default=".", help="directory to extract into")
|
|
12
|
+
p.set_defaults(func=run)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(args):
|
|
16
|
+
blob = read_capsule(args.file)
|
|
17
|
+
info = capsule.inspect(blob)
|
|
18
|
+
now = datetime.now(timezone.utc)
|
|
19
|
+
if not info.is_open(now):
|
|
20
|
+
raise SystemExit(
|
|
21
|
+
f"error: locked until {info.unlock_at.astimezone().isoformat()} "
|
|
22
|
+
f"({fmt_remaining(info.unlock_at - now)} remaining)"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
pw = None if info.cipher == "none" else ask_password()
|
|
26
|
+
names = capsule.open_capsule(blob, pw, args.dest)
|
|
27
|
+
print(_style.green(f"opened into {args.dest}/"))
|
|
28
|
+
for name in names:
|
|
29
|
+
print(f" {_style.dim(name)}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from magicicapsula.core import draft
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def register(sub):
|
|
5
|
+
p = sub.add_parser("rm", help="unstage files (does not delete them from disk)")
|
|
6
|
+
p.add_argument("paths", nargs="+", help="staged paths to drop from the capsule")
|
|
7
|
+
p.set_defaults(func=run)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(args):
|
|
11
|
+
d = draft.load()
|
|
12
|
+
removed = draft.remove(d, args.paths)
|
|
13
|
+
if not removed:
|
|
14
|
+
print("none of those were staged")
|
|
15
|
+
return
|
|
16
|
+
for p in removed:
|
|
17
|
+
print(f" unstaged {p}")
|