aboutsummaryrefslogtreecommitdiff
path: root/build/blog/2015-06-27-automatically-clean-up-previous-mobile-applications.html
blob: 29faf9ca0bae5743d6671b9ad4351667991772de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta content="pandoc" name="generator"/>
<meta content="Zhiming Wang" name="author"/>
<meta content="2015-06-27T21:19:59-07:00" name="date"/>
<title>Automatically clean up "Previous Mobile Applications"</title>
<link href="/img/apple-touch-icon-152.png" rel="apple-touch-icon-precomposed"/>
<meta content="#FFFFFF" name="msapplication-TileColor"/>
<meta content="/img/favicon-144.png" name="msapplication-TileImage"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="/css/normalize.min.css" media="all" rel="stylesheet" type="text/css"/>
<link href="/css/theme.css" media="all" rel="stylesheet" type="text/css"/>
<link href="/css/highlight.css" media="all" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="archival-notice">This blog has been archived.<br/>Visit my home page at <a href="https://zhimingwang.org">zhimingwang.org</a>.</div>
<nav class="nav">
<a class="nav-icon" href="/" title="Home"><!--blog icon--></a>
<a class="nav-title" href="/"><!--blog title--></a>
<a class="nav-author" href="https://github.com/zmwangx" target="_blank"><!--blog author--></a>
</nav>
<article class="content">
<header class="article-header">
<h1 class="article-title">Automatically clean up "Previous Mobile Applications"</h1>
<div class="article-metadata">
<time class="article-timestamp" datetime="2015-06-27T21:19:59-07:00">June 27, 2015</time>
</div>
</header>
<p>iTunes keeps a "Previous Mobile Applications" folder of questionable value, which always annoys me. It eats into disk space and wastes syncing/backup cycles and bandwidth; you can easily find horror stories online about <a href="http://forums.macrumors.com/threads/5-years-of-deleted-iphone-apps-accumulated-in-my-itunes-library.1781676/#post-19749496">100GB+ PMA folders</a>. The value? You might be able to roll back to an earlier version, or restore an app pulled from the App Store. Really? I never had that need in my life<a class="footnoteRef" href="#fn1" id="fnref1"><sup>1</sup></a>; have you? Worst of all, there should be a periodic clean up option — just like how deleted mail are automatically purged after one month, but the option is missing.</p>
<p>Therefore, I wrote a trivial Python script to do the periodic cleanup. Feel free to grab my script below (also available at <a class="uri" href="http://git.io/previous-mobile-applications">http://git.io/previous-mobile-applications</a>) to save a few minutes of hacking. It should be plugged into a daily or weekly or monthly cron job (or the equivalent), and it writes data to <code>~/.local/share/itunes/previous-mobile-applications.json</code> by default. To customize, just modify the global constants.</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="co">#!/usr/bin/env python3</span>

<span class="co">"""Periodically clean up "Previous Mobile Applications" of iTunes."""</span>

<span class="im">import</span> arrow
<span class="im">import</span> datetime
<span class="im">import</span> json
<span class="im">import</span> os
<span class="im">import</span> sys

OFFENDING_DIR <span class="op">=</span> os.path.expanduser(<span class="st">"~/Music/iTunes/iTunes Media/Mobile Applications/Previous Mobile Applications"</span>)
STORAGE_DIR <span class="op">=</span> os.path.expanduser(<span class="st">"~/.local/share/itunes"</span>)
STORAGE_FILE <span class="op">=</span> os.path.join(STORAGE_DIR, <span class="st">"previous-mobile-applications.json"</span>)

DELETE_AFTER <span class="op">=</span> datetime.timedelta(days<span class="op">=</span><span class="dv">7</span>)

<span class="kw">def</span> load_storage():
    <span class="co">"""Load stored dictionary of seen apps from STORAGE_FILE.</span>

<span class="co">    Returns</span>
<span class="co">    -------</span>
<span class="co">    seen_app_dict : dict</span>
<span class="co">        Dictionary of (app_filename, first_seen_date) key-value pairs,</span>
<span class="co">        where app_filename is str, and last_seen_date is datetime.date.</span>

<span class="co">    """</span>
    os.makedirs(STORAGE_DIR, mode<span class="op">=</span><span class="bn">0o700</span>, exist_ok<span class="op">=</span><span class="va">True</span>)
    <span class="cf">try</span>:
        <span class="cf">with</span> <span class="bu">open</span>(STORAGE_FILE, encoding<span class="op">=</span><span class="st">"utf-8"</span>) <span class="im">as</span> fp:
            serializable_seen_app_dict <span class="op">=</span> json.load(fp)
            <span class="cf">return</span> {app_filename: arrow.get(serialized_first_seen_date).date()
                    <span class="cf">for</span> app_filename, serialized_first_seen_date <span class="kw">in</span> serializable_seen_app_dict.items()}
    <span class="cf">except</span> <span class="pp">OSError</span>:
        <span class="cf">return</span> {}

<span class="kw">def</span> write_storage(seen_app_dict):
    <span class="co">"""Write the dictionary of seen apps to STORAGE_FILE.</span>

<span class="co">    Parameters</span>
<span class="co">    ----------</span>
<span class="co">    seen_app_dict : dict</span>
<span class="co">        See the return format of load_storage().</span>

<span class="co">    Returns</span>
<span class="co">    -------</span>
<span class="co">    0 or 1</span>
<span class="co">        Return code indicating success or failure.</span>

<span class="co">    """</span>
    <span class="co"># convert datetime.time to str (ISO 8601)</span>
    serializable_seen_app_dict <span class="op">=</span> {app_filename: first_seen_date.isoformat()
                                  <span class="cf">for</span> app_filename, first_seen_date <span class="kw">in</span> seen_app_dict.items()}
    os.makedirs(STORAGE_DIR, mode<span class="op">=</span><span class="bn">0o700</span>, exist_ok<span class="op">=</span><span class="va">True</span>)
    <span class="cf">try</span>:
        <span class="cf">with</span> <span class="bu">open</span>(STORAGE_FILE, mode<span class="op">=</span><span class="st">"w"</span>, encoding<span class="op">=</span><span class="st">"utf-8"</span>) <span class="im">as</span> fp:
            json.dump(serializable_seen_app_dict, fp, indent<span class="op">=</span><span class="dv">2</span>, sort_keys<span class="op">=</span><span class="va">True</span>)
        <span class="cf">return</span> <span class="dv">0</span>
    <span class="cf">except</span> <span class="pp">OSError</span> <span class="im">as</span> err:
        sys.stderr.write(<span class="st">"error: failed to write to '</span><span class="sc">%s</span><span class="st">': </span><span class="sc">%s</span><span class="st">"</span> <span class="op">%</span> (STORAGE_FILE, <span class="bu">str</span>(err)))
        <span class="cf">return</span> <span class="dv">1</span>

<span class="kw">def</span> main():
    <span class="co">"""Main.</span>

<span class="co">    Returns</span>
<span class="co">    -------</span>
<span class="co">    0 or 1</span>
<span class="co">        Return code indicating success or failure.</span>

<span class="co">    """</span>
    <span class="cf">if</span> <span class="kw">not</span> os.path.isdir(OFFENDING_DIR):
        <span class="co"># good, you don't have that junk</span>
        <span class="cf">return</span> <span class="dv">0</span>

    today <span class="op">=</span> datetime.date.today()
    seen_app_dict <span class="op">=</span> load_storage()
    current_app_list <span class="op">=</span> os.listdir(OFFENDING_DIR)

    <span class="co"># boot already disappeared apps</span>
    <span class="cf">for</span> app <span class="kw">in</span> [app <span class="cf">for</span> app <span class="kw">in</span> seen_app_dict <span class="cf">if</span> app <span class="kw">not</span> <span class="kw">in</span> current_app_list]:
        seen_app_dict.pop(app)

    <span class="co"># add newly appeared apps</span>
    <span class="cf">for</span> app <span class="kw">in</span> [app <span class="cf">for</span> app <span class="kw">in</span> current_app_list <span class="cf">if</span> app <span class="kw">not</span> <span class="kw">in</span> seen_app_dict]:
        seen_app_dict[app] <span class="op">=</span> today

    <span class="co"># delete expired apps</span>
    returncode <span class="op">=</span> <span class="dv">0</span>
    newly_deleted_apps <span class="op">=</span> []
    <span class="cf">for</span> app <span class="kw">in</span> seen_app_dict:
        <span class="cf">if</span> today <span class="op">&gt;=</span> seen_app_dict[app] <span class="op">+</span> DELETE_AFTER:
            app_path <span class="op">=</span> os.path.join(OFFENDING_DIR, app)
            <span class="cf">try</span>:
                os.remove(app_path)
                newly_deleted_apps.append(app)
            <span class="cf">except</span> <span class="pp">OSError</span> <span class="im">as</span> err:
                sys.stderr.write(<span class="st">"error: failed to remove '</span><span class="sc">%s</span><span class="st">': </span><span class="sc">%s</span><span class="st">"</span> <span class="op">%</span> (app_path, <span class="bu">str</span>(err)))
                returncode <span class="op">=</span> <span class="dv">1</span>

    <span class="cf">for</span> app <span class="kw">in</span> newly_deleted_apps:
        seen_app_dict.pop(app)

    <span class="co"># write data to disk</span>
    returncode <span class="op">|=</span> write_storage(seen_app_dict)

    <span class="cf">return</span> returncode

<span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">"__main__"</span>:
    exit(main())</code></pre></div>
<div class="footnotes">
<hr/>
<ol>
<li id="fn1"><p>Full disclosure: unlike many people, I'm not very obsessed with my phone, and I only have about two dozen third-party apps.<a class="footnotes-backlink" href="#fnref1">↩︎</a></p></li>
</ol>
</div>
</article>
<hr class="content-separator"/>
<footer class="footer">
<span class="rfooter">
<a class="rss-icon" href="/rss.xml" target="_blank" title="RSS feed"><!--RSS feed icon--></a><a class="atom-icon" href="/atom.xml" target="_blank" title="Atom feed"><!--Atom feed icon--></a><a class="cc-icon" href="https://creativecommons.org/licenses/by/4.0/" target="_blank" title="Released under the Creative Commons Attribution 4.0 International license."><!--CC icon--></a>
<a href="https://github.com/zmwangx" target="_blank">Zhiming Wang</a>
</span>
</footer>
</body>
</html>