Support compressed package files
[ntk/apt.git] / cmdline / apt-cdrom.cc
1 // -*- mode: cpp; mode: fold -*-
2 // Description /*{{{*/
3 // $Id: apt-cdrom.cc,v 1.30 1999/08/12 07:00:55 jgg Exp $
4 /* ######################################################################
5
6 APT CDROM - Tool for handling APT's CDROM database.
7
8 Currently the only option is 'add' which will take the current CD
9 in the drive and add it into the database.
10
11 ##################################################################### */
12 /*}}}*/
13 // Include Files /*{{{*/
14 #include <apt-pkg/cmndline.h>
15 #include <apt-pkg/error.h>
16 #include <apt-pkg/init.h>
17 #include <apt-pkg/fileutl.h>
18 #include <apt-pkg/progress.h>
19 #include <apt-pkg/cdromutl.h>
20 #include <apt-pkg/strutl.h>
21 #include <config.h>
22
23 #include "indexcopy.h"
24
25 #include <iostream>
26 #include <fstream>
27 #include <vector>
28 #include <algorithm>
29 #include <sys/stat.h>
30 #include <fcntl.h>
31 #include <dirent.h>
32 #include <unistd.h>
33 #include <stdio.h>
34 /*}}}*/
35
36 // FindPackages - Find the package files on the CDROM /*{{{*/
37 // ---------------------------------------------------------------------
38 /* We look over the cdrom for package files. This is a recursive
39 search that short circuits when it his a package file in the dir.
40 This speeds it up greatly as the majority of the size is in the
41 binary-* sub dirs. */
42 bool FindPackages(string CD,vector<string> &List,vector<string> &SList,
43 string &InfoDir,unsigned int Depth = 0)
44 {
45 static ino_t Inodes[9];
46 if (Depth >= 7)
47 return true;
48
49 if (CD[CD.length()-1] != '/')
50 CD += '/';
51
52 if (chdir(CD.c_str()) != 0)
53 return _error->Errno("chdir","Unable to change to %s",CD.c_str());
54
55 // Look for a .disk subdirectory
56 struct stat Buf;
57 if (stat(".disk",&Buf) == 0)
58 {
59 if (InfoDir.empty() == true)
60 InfoDir = CD + ".disk/";
61 }
62
63 /* Aha! We found some package files. We assume that everything under
64 this dir is controlled by those package files so we don't look down
65 anymore */
66 if (stat("Packages",&Buf) == 0 || stat("Packages.gz",&Buf))
67 {
68 List.push_back(CD);
69
70 // Continue down if thorough is given
71 if (_config->FindB("APT::CDROM::Thorough",false) == false)
72 return true;
73 }
74 if (stat("Sources.gz",&Buf) == 0 || stat("Sources",&Buf) == 0)
75 {
76 SList.push_back(CD);
77
78 // Continue down if thorough is given
79 if (_config->FindB("APT::CDROM::Thorough",false) == false)
80 return true;
81 }
82
83 DIR *D = opendir(".");
84 if (D == 0)
85 return _error->Errno("opendir","Unable to read %s",CD.c_str());
86
87 // Run over the directory
88 for (struct dirent *Dir = readdir(D); Dir != 0; Dir = readdir(D))
89 {
90 // Skip some files..
91 if (strcmp(Dir->d_name,".") == 0 ||
92 strcmp(Dir->d_name,"..") == 0 ||
93 //strcmp(Dir->d_name,"source") == 0 ||
94 strcmp(Dir->d_name,"experimental") == 0 ||
95 strcmp(Dir->d_name,"binary-all") == 0)
96 continue;
97
98 // See if the name is a sub directory
99 struct stat Buf;
100 if (stat(Dir->d_name,&Buf) != 0)
101 continue;
102
103 if (S_ISDIR(Buf.st_mode) == 0)
104 continue;
105
106 unsigned int I;
107 for (I = 0; I != Depth; I++)
108 if (Inodes[I] == Buf.st_ino)
109 break;
110 if (I != Depth)
111 continue;
112
113 // Store the inodes weve seen
114 Inodes[Depth] = Buf.st_ino;
115
116 // Descend
117 if (FindPackages(CD + Dir->d_name,List,SList,InfoDir,Depth+1) == false)
118 break;
119
120 if (chdir(CD.c_str()) != 0)
121 return _error->Errno("chdir","Unable to change to ",CD.c_str());
122 };
123
124 closedir(D);
125
126 return !_error->PendingError();
127 }
128 /*}}}*/
129 // DropBinaryArch - Dump dirs with a string like /binary-<foo>/ /*{{{*/
130 // ---------------------------------------------------------------------
131 /* Here we drop everything that is not this machines arch */
132 bool DropBinaryArch(vector<string> &List)
133 {
134 char S[300];
135 sprintf(S,"/binary-%s/",_config->Find("Apt::Architecture").c_str());
136
137 for (unsigned int I = 0; I < List.size(); I++)
138 {
139 const char *Str = List[I].c_str();
140
141 const char *Res;
142 if ((Res = strstr(Str,"/binary-")) == 0)
143 continue;
144
145 // Weird, remove it.
146 if (strlen(Res) < strlen(S))
147 {
148 List.erase(List.begin() + I);
149 I--;
150 continue;
151 }
152
153 // See if it is our arch
154 if (stringcmp(Res,Res + strlen(S),S) == 0)
155 continue;
156
157 // Erase it
158 List.erase(List.begin() + I);
159 I--;
160 }
161
162 return true;
163 }
164 /*}}}*/
165 // Score - We compute a 'score' for a path /*{{{*/
166 // ---------------------------------------------------------------------
167 /* Paths are scored based on how close they come to what I consider
168 normal. That is ones that have 'dist' 'stable' 'frozen' will score
169 higher than ones without. */
170 int Score(string Path)
171 {
172 int Res = 0;
173 if (Path.find("stable/") != string::npos)
174 Res += 2;
175 if (Path.find("/binary-") != string::npos)
176 Res += 2;
177 if (Path.find("frozen/") != string::npos)
178 Res += 2;
179 if (Path.find("/dists/") != string::npos)
180 Res += 4;
181 if (Path.find("/main/") != string::npos)
182 Res += 2;
183 if (Path.find("/contrib/") != string::npos)
184 Res += 2;
185 if (Path.find("/non-free/") != string::npos)
186 Res += 2;
187 if (Path.find("/non-US/") != string::npos)
188 Res += 2;
189 if (Path.find("/source/") != string::npos)
190 Res += 1;
191 return Res;
192 }
193 /*}}}*/
194 // DropRepeats - Drop repeated files resulting from symlinks /*{{{*/
195 // ---------------------------------------------------------------------
196 /* Here we go and stat every file that we found and strip dup inodes. */
197 bool DropRepeats(vector<string> &List,const char *Name)
198 {
199 // Get a list of all the inodes
200 ino_t *Inodes = new ino_t[List.size()];
201 for (unsigned int I = 0; I != List.size(); I++)
202 {
203 struct stat Buf;
204 if (stat((List[I] + Name).c_str(),&Buf) != 0 &&
205 stat((List[I] + Name + ".gz").c_str(),&Buf) != 0)
206 _error->Errno("stat","Failed to stat %s%s",List[I].c_str(),
207 Name);
208 Inodes[I] = Buf.st_ino;
209 }
210
211 if (_error->PendingError() == true)
212 return false;
213
214 // Look for dups
215 for (unsigned int I = 0; I != List.size(); I++)
216 {
217 for (unsigned int J = I+1; J < List.size(); J++)
218 {
219 // No match
220 if (Inodes[J] != Inodes[I])
221 continue;
222
223 // We score the two paths.. and erase one
224 int ScoreA = Score(List[I]);
225 int ScoreB = Score(List[J]);
226 if (ScoreA < ScoreB)
227 {
228 List[I] = string();
229 break;
230 }
231
232 List[J] = string();
233 }
234 }
235
236 // Wipe erased entries
237 for (unsigned int I = 0; I < List.size();)
238 {
239 if (List[I].empty() == false)
240 I++;
241 else
242 List.erase(List.begin()+I);
243 }
244
245 return true;
246 }
247 /*}}}*/
248
249 // ReduceSourceList - Takes the path list and reduces it /*{{{*/
250 // ---------------------------------------------------------------------
251 /* This takes the list of source list expressed entires and collects
252 similar ones to form a single entry for each dist */
253 bool ReduceSourcelist(string CD,vector<string> &List)
254 {
255 sort(List.begin(),List.end());
256
257 // Collect similar entries
258 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
259 {
260 // Find a space..
261 string::size_type Space = (*I).find(' ');
262 if (Space == string::npos)
263 continue;
264 string::size_type SSpace = (*I).find(' ',Space + 1);
265 if (SSpace == string::npos)
266 continue;
267
268 string Word1 = string(*I,Space,SSpace-Space);
269 for (vector<string>::iterator J = List.begin(); J != I; J++)
270 {
271 // Find a space..
272 string::size_type Space2 = (*J).find(' ');
273 if (Space2 == string::npos)
274 continue;
275 string::size_type SSpace2 = (*J).find(' ',Space2 + 1);
276 if (SSpace2 == string::npos)
277 continue;
278
279 if (string(*J,Space2,SSpace2-Space2) != Word1)
280 continue;
281
282 *J += string(*I,SSpace);
283 *I = string();
284 }
285 }
286
287 // Wipe erased entries
288 for (unsigned int I = 0; I < List.size();)
289 {
290 if (List[I].empty() == false)
291 I++;
292 else
293 List.erase(List.begin()+I);
294 }
295 }
296 /*}}}*/
297 // WriteDatabase - Write the CDROM Database file /*{{{*/
298 // ---------------------------------------------------------------------
299 /* We rewrite the configuration class associated with the cdrom database. */
300 bool WriteDatabase(Configuration &Cnf)
301 {
302 string DFile = _config->FindFile("Dir::State::cdroms");
303 string NewFile = DFile + ".new";
304
305 unlink(NewFile.c_str());
306 ofstream Out(NewFile.c_str());
307 if (!Out)
308 return _error->Errno("ofstream::ofstream",
309 "Failed to open %s.new",DFile.c_str());
310
311 /* Write out all of the configuration directives by walking the
312 configuration tree */
313 const Configuration::Item *Top = Cnf.Tree(0);
314 for (; Top != 0;)
315 {
316 // Print the config entry
317 if (Top->Value.empty() == false)
318 Out << Top->FullTag() + " \"" << Top->Value << "\";" << endl;
319
320 if (Top->Child != 0)
321 {
322 Top = Top->Child;
323 continue;
324 }
325
326 while (Top != 0 && Top->Next == 0)
327 Top = Top->Parent;
328 if (Top != 0)
329 Top = Top->Next;
330 }
331
332 Out.close();
333
334 rename(DFile.c_str(),string(DFile + '~').c_str());
335 if (rename(NewFile.c_str(),DFile.c_str()) != 0)
336 return _error->Errno("rename","Failed to rename %s.new to %s",
337 DFile.c_str(),DFile.c_str());
338
339 return true;
340 }
341 /*}}}*/
342 // WriteSourceList - Write an updated sourcelist /*{{{*/
343 // ---------------------------------------------------------------------
344 /* This reads the old source list and copies it into the new one. It
345 appends the new CDROM entires just after the first block of comments.
346 This places them first in the file. It also removes any old entries
347 that were the same. */
348 bool WriteSourceList(string Name,vector<string> &List,bool Source)
349 {
350 if (List.size() == 0)
351 return true;
352
353 string File = _config->FindFile("Dir::Etc::sourcelist");
354
355 // Open the stream for reading
356 ifstream F(File.c_str(),ios::in | ios::nocreate);
357 if (!F != 0)
358 return _error->Errno("ifstream::ifstream","Opening %s",File.c_str());
359
360 string NewFile = File + ".new";
361 unlink(NewFile.c_str());
362 ofstream Out(NewFile.c_str());
363 if (!Out)
364 return _error->Errno("ofstream::ofstream",
365 "Failed to open %s.new",File.c_str());
366
367 // Create a short uri without the path
368 string ShortURI = "cdrom:" + Name + "/";
369
370 const char *Type;
371 if (Source == true)
372 Type = "deb-src";
373 else
374 Type = "deb";
375
376 char Buffer[300];
377 int CurLine = 0;
378 bool First = true;
379 while (F.eof() == false)
380 {
381 F.getline(Buffer,sizeof(Buffer));
382 CurLine++;
383 _strtabexpand(Buffer,sizeof(Buffer));
384 _strstrip(Buffer);
385
386 // Comment or blank
387 if (Buffer[0] == '#' || Buffer[0] == 0)
388 {
389 Out << Buffer << endl;
390 continue;
391 }
392
393 if (First == true)
394 {
395 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
396 {
397 string::size_type Space = (*I).find(' ');
398 if (Space == string::npos)
399 return _error->Error("Internal error");
400 Out << Type << " \"cdrom:" << Name << "/" << string(*I,0,Space) <<
401 "\" " << string(*I,Space+1) << endl;
402 }
403 }
404 First = false;
405
406 // Grok it
407 string cType;
408 string URI;
409 const char *C = Buffer;
410 if (ParseQuoteWord(C,cType) == false ||
411 ParseQuoteWord(C,URI) == false)
412 {
413 Out << Buffer << endl;
414 continue;
415 }
416
417 // Emit lines like this one
418 if (cType != Type || string(URI,0,ShortURI.length()) != ShortURI)
419 {
420 Out << Buffer << endl;
421 continue;
422 }
423 }
424
425 // Just in case the file was empty
426 if (First == true)
427 {
428 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
429 {
430 string::size_type Space = (*I).find(' ');
431 if (Space == string::npos)
432 return _error->Error("Internal error");
433
434 Out << "deb \"cdrom:" << Name << "/" << string(*I,0,Space) <<
435 "\" " << string(*I,Space+1) << endl;
436 }
437 }
438
439 Out.close();
440
441 rename(File.c_str(),string(File + '~').c_str());
442 if (rename(NewFile.c_str(),File.c_str()) != 0)
443 return _error->Errno("rename","Failed to rename %s.new to %s",
444 File.c_str(),File.c_str());
445
446 return true;
447 }
448 /*}}}*/
449
450 // Prompt - Simple prompt /*{{{*/
451 // ---------------------------------------------------------------------
452 /* */
453 void Prompt(const char *Text)
454 {
455 char C;
456 cout << Text << ' ' << flush;
457 read(STDIN_FILENO,&C,1);
458 if (C != '\n')
459 cout << endl;
460 }
461 /*}}}*/
462 // PromptLine - Prompt for an input line /*{{{*/
463 // ---------------------------------------------------------------------
464 /* */
465 string PromptLine(const char *Text)
466 {
467 cout << Text << ':' << endl;
468
469 string Res;
470 getline(cin,Res);
471 return Res;
472 }
473 /*}}}*/
474
475 // DoAdd - Add a new CDROM /*{{{*/
476 // ---------------------------------------------------------------------
477 /* This does the main add bit.. We show some status and things. The
478 sequence is to mount/umount the CD, Ident it then scan it for package
479 files and reduce that list. Then we copy over the package files and
480 verify them. Then rewrite the database files */
481 bool DoAdd(CommandLine &)
482 {
483 // Startup
484 string CDROM = _config->FindDir("Acquire::cdrom::mount","/cdrom/");
485 if (CDROM[0] == '.')
486 CDROM= SafeGetCWD() + '/' + CDROM;
487
488 cout << "Using CD-ROM mount point " << CDROM << endl;
489
490 // Read the database
491 Configuration Database;
492 string DFile = _config->FindFile("Dir::State::cdroms");
493 if (FileExists(DFile) == true)
494 {
495 if (ReadConfigFile(Database,DFile) == false)
496 return _error->Error("Unable to read the cdrom database %s",
497 DFile.c_str());
498 }
499
500 // Unmount the CD and get the user to put in the one they want
501 if (_config->FindB("APT::CDROM::NoMount",false) == false)
502 {
503 cout << "Unmounting CD-ROM" << endl;
504 UnmountCdrom(CDROM);
505
506 // Mount the new CDROM
507 Prompt("Please insert a Disc in the drive and press enter");
508 cout << "Mounting CD-ROM" << endl;
509 if (MountCdrom(CDROM) == false)
510 return _error->Error("Failed to mount the cdrom.");
511 }
512
513 // Hash the CD to get an ID
514 cout << "Identifying.. " << flush;
515 string ID;
516 if (IdentCdrom(CDROM,ID) == false)
517 {
518 cout << endl;
519 return false;
520 }
521
522 cout << '[' << ID << ']' << endl;
523
524 cout << "Scanning Disc for index files.. " << flush;
525 // Get the CD structure
526 vector<string> List;
527 vector<string> sList;
528 string StartDir = SafeGetCWD();
529 string InfoDir;
530 if (FindPackages(CDROM,List,sList,InfoDir) == false)
531 {
532 cout << endl;
533 return false;
534 }
535
536 chdir(StartDir.c_str());
537
538 if (_config->FindB("Debug::aptcdrom",false) == true)
539 {
540 cout << "I found:" << endl;
541 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
542 {
543 cout << *I << endl;
544 }
545 }
546
547 // Fix up the list
548 DropBinaryArch(List);
549 DropRepeats(List,"Packages");
550 DropRepeats(sList,"Sources");
551 cout << "Found " << List.size() << " package indexes and " << sList.size() <<
552 " source indexes." << endl;
553
554 if (List.size() == 0)
555 return _error->Error("Unable to locate any package files, perhaps this is not a Debian Disc");
556
557 // Check if the CD is in the database
558 string Name;
559 if (Database.Exists("CD::" + ID) == false ||
560 _config->FindB("APT::CDROM::Rename",false) == true)
561 {
562 // Try to use the CDs label if at all possible
563 if (InfoDir.empty() == false &&
564 FileExists(InfoDir + "/info") == true)
565 {
566 ifstream F(string(InfoDir + "/info").c_str());
567 if (!F == 0)
568 getline(F,Name);
569
570 if (Name.empty() == false)
571 {
572 cout << "Found label '" << Name << "'" << endl;
573 Database.Set("CD::" + ID + "::Label",Name);
574 }
575 }
576
577 if (_config->FindB("APT::CDROM::Rename",false) == true ||
578 Name.empty() == true)
579 {
580 cout << "Please provide a name for this Disc, such as 'Debian 2.1r1 Disk 1'";
581 while (1)
582 {
583 Name = PromptLine("");
584 if (Name.empty() == false &&
585 Name.find('"') == string::npos &&
586 Name.find(':') == string::npos &&
587 Name.find('/') == string::npos)
588 break;
589 cout << "That is not a valid name, try again " << endl;
590 }
591 }
592 }
593 else
594 Name = Database.Find("CD::" + ID);
595
596 string::iterator J = Name.begin();
597 for (; J != Name.end(); J++)
598 if (*J == '/' || *J == '"' || *J == ':')
599 *J = '_';
600
601 Database.Set("CD::" + ID,Name);
602 cout << "This Disc is called '" << Name << "'" << endl;
603
604 // Copy the package files to the state directory
605 PackageCopy Copy;
606 SourceCopy SrcCopy;
607 if (Copy.CopyPackages(CDROM,Name,List) == false ||
608 SrcCopy.CopyPackages(CDROM,Name,sList) == false)
609 return false;
610
611 ReduceSourcelist(CDROM,List);
612 ReduceSourcelist(CDROM,sList);
613
614 // Write the database and sourcelist
615 if (_config->FindB("APT::cdrom::NoAct",false) == false)
616 {
617 if (WriteDatabase(Database) == false)
618 return false;
619
620 cout << "Writing new source list" << endl;
621 if (WriteSourceList(Name,List,false) == false ||
622 WriteSourceList(Name,sList,true) == false)
623 return false;
624 }
625
626 // Print the sourcelist entries
627 cout << "Source List entries for this Disc are:" << endl;
628 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
629 {
630 string::size_type Space = (*I).find(' ');
631 if (Space == string::npos)
632 return _error->Error("Internal error");
633
634 cout << "deb \"cdrom:" << Name << "/" << string(*I,0,Space) <<
635 "\" " << string(*I,Space+1) << endl;
636 }
637
638 for (vector<string>::iterator I = sList.begin(); I != sList.end(); I++)
639 {
640 string::size_type Space = (*I).find(' ');
641 if (Space == string::npos)
642 return _error->Error("Internal error");
643
644 cout << "deb-src \"cdrom:" << Name << "/" << string(*I,0,Space) <<
645 "\" " << string(*I,Space+1) << endl;
646 }
647
648 cout << "Repeat this process for the rest of the CDs in your set." << endl;
649 return true;
650 }
651 /*}}}*/
652
653 // ShowHelp - Show the help screen /*{{{*/
654 // ---------------------------------------------------------------------
655 /* */
656 int ShowHelp()
657 {
658 cout << PACKAGE << ' ' << VERSION << " for " << ARCHITECTURE <<
659 " compiled on " << __DATE__ << " " << __TIME__ << endl;
660 if (_config->FindB("version") == true)
661 return 100;
662
663 cout << "Usage: apt-cdrom [options] command" << endl;
664 cout << endl;
665 cout << "apt-cdrom is a tool to add CDROM's to APT's source list. The " << endl;
666 cout << "CDROM mount point and device information is taken from apt.conf" << endl;
667 cout << "and /etc/fstab." << endl;
668 cout << endl;
669 cout << "Commands:" << endl;
670 cout << " add - Add a CDROM" << endl;
671 cout << endl;
672 cout << "Options:" << endl;
673 cout << " -h This help text" << endl;
674 cout << " -d CD-ROM mount point" << endl;
675 cout << " -r Rename a recognized CD-ROM" << endl;
676 cout << " -m No mounting" << endl;
677 cout << " -f Fast mode, don't check package files" << endl;
678 cout << " -a Thorough scan mode" << endl;
679 cout << " -c=? Read this configuration file" << endl;
680 cout << " -o=? Set an arbitary configuration option, eg -o dir::cache=/tmp" << endl;
681 cout << "See fstab(5)" << endl;
682 return 100;
683 }
684 /*}}}*/
685
686 int main(int argc,const char *argv[])
687 {
688 CommandLine::Args Args[] = {
689 {'h',"help","help",0},
690 {'v',"version","version",0},
691 {'d',"cdrom","Acquire::cdrom::mount",CommandLine::HasArg},
692 {'r',"rename","APT::CDROM::Rename",0},
693 {'m',"no-mount","APT::CDROM::NoMount",0},
694 {'f',"fast","APT::CDROM::Fast",0},
695 {'n',"just-print","APT::CDROM::NoAct",0},
696 {'n',"recon","APT::CDROM::NoAct",0},
697 {'n',"no-act","APT::CDROM::NoAct",0},
698 {'a',"thorough","APT::CDROM::Thorough",0},
699 {'c',"config-file",0,CommandLine::ConfigFile},
700 {'o',"option",0,CommandLine::ArbItem},
701 {0,0,0,0}};
702 CommandLine::Dispatch Cmds[] = {
703 {"add",&DoAdd},
704 {0,0}};
705
706 // Parse the command line and initialize the package library
707 CommandLine CmdL(Args,_config);
708 if (pkgInitialize(*_config) == false ||
709 CmdL.Parse(argc,argv) == false)
710 {
711 _error->DumpErrors();
712 return 100;
713 }
714
715 // See if the help should be shown
716 if (_config->FindB("help") == true ||
717 CmdL.FileSize() == 0)
718 return ShowHelp();
719
720 // Deal with stdout not being a tty
721 if (ttyname(STDOUT_FILENO) == 0 && _config->FindI("quiet",0) < 1)
722 _config->Set("quiet","1");
723
724 // Match the operation
725 CmdL.DispatchArg(Cmds);
726
727 // Print any errors or warnings found during parsing
728 if (_error->empty() == false)
729 {
730 bool Errors = _error->PendingError();
731 _error->DumpErrors();
732 return Errors == true?100:0;
733 }
734
735 return 0;
736 }