-
Notifications
You must be signed in to change notification settings - Fork 44
/
FileChanges.rb
280 lines (246 loc) · 9.44 KB
/
FileChanges.rb
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File: modules/FileChanges.ycp
# Module: yast2
# Summary: Detect if a configuratil file was changed
# Authors: Jiri Srain <jsrain@suse.cz>
#
# Support routines for detecting changes of configuration files being done
# externally (not by YaST) to prevent the changes from being lost because
# of YaST not handling the configuration files correctly (eg. removing
# comments in some cases, changing order of options,...)
#
# Warns user if such change is detected.
#
# Usage:
# Before reading the configuration file:
# call boolean CheckFiles (list<string>) with all files. If any of them
# is detected to be changed, YaST asks a popup for you.
# alternatively use boolean FileChanged (string) for each file (does not
# ask any question, immediatelly returns status of the file
#
# After writing the configuraiton file:
# call void StoreFileCheckSum (string) for each file to store recent
# checksum. YaST will use this checksum next time checking.
#
require "yast"
require "shellwords"
module Yast
class FileChangesClass < Module
def main
Yast.import "UI"
textdomain "base"
Yast.import "Mode"
Yast.import "Popup"
Yast.import "Directory"
Yast.import "Label"
Yast.import "FileUtils"
@data_file = "/var/lib/YaST2/file_checksums.ycp"
@file_checksums = {}
end
# Read the data file containing file checksums
def ReadSettings
if Ops.less_or_equal(
Convert.to_integer(SCR.Read(path(".target.size"), @data_file)),
0
)
@file_checksums = {}
return
end
@file_checksums = Convert.convert(
SCR.Read(path(".target.ycp"), @data_file),
from: "any",
to: "map <string, string>"
)
@file_checksums = {} if @file_checksums.nil?
nil
end
# Write the data file containing checksums
def WriteSettings
SCR.Write(path(".target.ycp"), @data_file, @file_checksums)
nil
end
# Compute the checksum of a file
# @param [String] file string the file to compute checksum of
# @return [String] the checksum
def ComputeFileChecksum(file)
# See also FileUtils::MD5sum()
cmd = Builtins.sformat("/usr/bin/md5sum %1", file.shellescape)
out = Convert.to_map(SCR.Execute(path(".target.bash_output"), cmd))
# NOTE: it also contains file name, but since it is only to be compared
# it does not matter
Ops.get_string(out, "stdout", "")
end
# Check if file was modified compared to the one distributed
# with the RPM package
# @param [String] file string the file to check
# @return [Boolean] true of was changed
def FileChangedFromPackage(file)
# queryformat: no trailing newline!
cmd = Builtins.sformat(
"/usr/bin/rpm -qf %1 --qf %%{NAME}-%%{VERSION}-%%{RELEASE}",
file.shellescape
)
out = Convert.to_map(SCR.Execute(path(".target.bash_output"), cmd))
package = Ops.get_string(out, "stdout", "")
Builtins.y2milestone("Package owning %1: %2", file, package)
return false if package == "" || Ops.get_integer(out, "exit", -1) != 0
cmd = Builtins.sformat("/usr/bin/rpm -V %1 |grep %2", package.shellescape, " #{file}$".shellescape)
out = Convert.to_map(SCR.Execute(path(".target.bash_output"), cmd))
changes = Ops.get_string(out, "stdout", "")
Builtins.y2milestone("File possibly changed: %1", changes)
lines = Builtins.splitstring(changes, "\n")
changed = false
Builtins.foreach(lines) do |line|
changed = true if Builtins.regexpmatch(line, "^S")
changed = true if Builtins.regexpmatch(line, "^..5")
changed = true if Builtins.regexpmatch(line, "^.......T")
end
changed
end
# Check if a file was modified externally (without YaST)
# @param [String] file string boolean the file to check
# @return [Boolean] true if was changed externally
def FileChanged(file)
# when generating AutoYaST configuration, they are not written back
return false if Mode.config
ReadSettings()
ret = false
if Builtins.haskey(@file_checksums, file)
Builtins.y2milestone("Comparing file %1 to stored checksum", file)
sum = ComputeFileChecksum(file)
ret = sum != Ops.get(@file_checksums, file, "")
else
Builtins.y2milestone("Comparing file %1 to RPM database", file)
ret = FileChangedFromPackage(file)
end
Builtins.y2milestone("File differs: %1", ret)
ret
end
# Store checksum of a file to the store
# @param [String] file string filename to compute and store
def StoreFileCheckSum(file)
ReadSettings()
sum = ComputeFileChecksum(file)
Ops.set(@file_checksums, file, sum)
WriteSettings()
nil
end
# Check files if any of them were changed
# Issue a question whether to continue if some were chaned
# @param [Array<String>] files a list of files to check
# @return [Boolean] true if either none was changed or user agreed
# to continue
def CheckFiles(files)
files = deep_copy(files)
files = Builtins.filter(files) { |f| FileChanged(f) }
return true unless Ops.greater_than(Builtins.size(files), 0)
msg = n_(
# Continue/Cancel question, %1 is a coma separated list of file names
_("Files %1 have been changed manually.\nYaST might lose some of the changes"),
# Continue/Cancel question, %1 is a file name
_("File %1 has been changed manually.\nYaST might lose some of the changes.\n"),
files.size
)
msg = Builtins.sformat(msg, Builtins.mergestring(files, ", "))
popup_file = "/filechecks_non_verbose"
stat = SCR.Read(path(".target.stat"), Ops.add(Directory.vardir, popup_file))
return true unless stat == {}
content = VBox(
Label(msg),
Left(CheckBox(Id(:disable), _("Do not show this message anymore"))),
ButtonBox(
PushButton(Id(:ok), Opt(:okButton), Label.ContinueButton),
PushButton(Id(:cancel), Opt(:cancelButton), Label.CancelButton)
)
)
UI.OpenDialog(content)
UI.SetFocus(:ok)
ret = UI.UserInput
Builtins.y2milestone("ret = %1", ret)
if ret == :ok && Convert.to_boolean(UI.QueryWidget(:disable, :Value))
Builtins.y2milestone("Disabled checksum popups")
SCR.Write(
path(".target.string"),
Ops.add(Directory.vardir, popup_file),
""
)
end
UI.CloseDialog
ret == :ok
end
# Files that are really new
#
# @param files [Array<String>] candidate files that may be new
# @return [Array<String>]
def created_files(files)
files - @file_checksums.keys
end
# Check if any of the possibly new created files is really new
# Issue a question whether to continue if such file was manually created
# @param [Array<String>] files a list of files to check
# @return [Boolean] true if either none was changed or user agreed
# to continue
def CheckNewCreatedFiles(files)
new_files = created_files(files)
return true if new_files.empty?
# TRANSLATORS: Continue/Cancel question, %s is a single file name or
# a comma separated list of file names.
msg = n_(
"File %s has been created manually.\nYaST might lose this file.",
"Files %s have been created manually.\nYaST might lose these files.",
new_files.size
) % new_files.join(", ")
popup_file = "/filechecks_non_verbose"
popup_file_path = File.join(Directory.vardir, popup_file)
return true if FileUtils.Exists(popup_file_path)
content = VBox(
Label(msg),
Left(CheckBox(Id(:disable), Message.DoNotShowMessageAgain())),
ButtonBox(
PushButton(Id(:ok), Opt(:okButton), Label.ContinueButton()),
PushButton(Id(:cancel), Opt(:cancelButton), Label.CancelButton())
)
)
UI.OpenDialog(content)
UI.SetFocus(:ok)
ret = UI.UserInput
Builtins.y2milestone("ret = %1", ret)
if ret == :ok && UI.QueryWidget(:disable, :Value)
Builtins.y2milestone("Disabled checksum popups")
SCR.Write(
path(".target.string"),
popup_file_path,
""
)
end
UI.CloseDialog
ret == :ok
end
publish function: :FileChanged, type: "boolean (string)"
publish function: :StoreFileCheckSum, type: "void (string)"
publish function: :CheckFiles, type: "boolean (list <string>)"
publish function: :CheckNewCreatedFiles, type: "boolean (list <string>)"
end
FileChanges = FileChangesClass.new
FileChanges.main
end