1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::System::Console::Command::Admin::Article::StorageSwitch;
10
11use strict;
12use warnings;
13
14use parent qw(Kernel::System::Console::BaseCommand);
15
16use Time::HiRes qw(usleep);
17
18our @ObjectDependencies = (
19    'Kernel::Config',
20    'Kernel::System::DateTime',
21    'Kernel::System::PID',
22    'Kernel::System::Ticket',
23);
24
25sub Configure {
26    my ( $Self, %Param ) = @_;
27
28    $Self->Description('Migrate article files from one storage backend to another on the fly.');
29    $Self->AddOption(
30        Name        => 'target',
31        Description => "Specify the target backend to migrate to (ArticleStorageDB|ArticleStorageFS).",
32        Required    => 1,
33        HasValue    => 1,
34        ValueRegex  => qr/^(?:ArticleStorageDB|ArticleStorageFS)$/smx,
35    );
36    $Self->AddOption(
37        Name        => 'tickets-closed-before-date',
38        Description => "Only process tickets closed before given ISO date.",
39        Required    => 0,
40        HasValue    => 1,
41        ValueRegex  => qr/^\d{4}-\d{2}-\d{2}[ ]\d{2}:\d{2}:\d{2}$/smx,
42    );
43    $Self->AddOption(
44        Name        => 'tickets-closed-before-days',
45        Description => "Only process tickets closed more than ... days ago.",
46        Required    => 0,
47        HasValue    => 1,
48        ValueRegex  => qr/^\d+$/smx,
49    );
50    $Self->AddOption(
51        Name        => 'tolerant',
52        Description => "Continue after failures.",
53        Required    => 0,
54        HasValue    => 0,
55    );
56    $Self->AddOption(
57        Name        => 'micro-sleep',
58        Description => "Specify microseconds to sleep after every ticket to reduce system load (e.g. 1000).",
59        Required    => 0,
60        HasValue    => 1,
61        ValueRegex  => qr/^\d+$/smx,
62    );
63    $Self->AddOption(
64        Name        => 'force-pid',
65        Description => "Start even if another process is still registered in the database.",
66        Required    => 0,
67        HasValue    => 0,
68    );
69
70    my $Name = $Self->Name();
71
72    $Self->AdditionalHelp(<<"EOF");
73The <green>$Name</green> command migrates article data from one storage backend to another on the fly, for example from DB to FS:
74
75 <green>otrs.Console.pl $Self->{Name} --target ArticleStorageFS</green>
76
77You can specify limits for the tickets migrated with <yellow>--tickets-closed-before-date</yellow> and <yellow>--tickets-closed-before-days</yellow>.
78
79To reduce load on the database for a running system, you can use the <yellow>--micro-sleep</yellow> parameter. The command will pause for the specified amount of microseconds after each ticket.
80
81 <green>otrs.Console.pl $Self->{Name} --target ArticleStorageFS --micro-sleep 1000</green>
82EOF
83    return;
84}
85
86sub PreRun {
87    my ($Self) = @_;
88
89    my $PIDCreated = $Kernel::OM->Get('Kernel::System::PID')->PIDCreate(
90        Name  => $Self->Name(),
91        Force => $Self->GetOption('force-pid'),
92        TTL   => 60 * 60 * 24 * 3,
93    );
94    if ( !$PIDCreated ) {
95        my $Error = "Unable to register the process in the database. Is another instance still running?\n";
96        $Error .= "You can use --force-pid to override this check.\n";
97        die $Error;
98    }
99
100    return;
101}
102
103sub Run {
104    my ($Self) = @_;
105
106    # disable ticket events
107    $Kernel::OM->Get('Kernel::Config')->{'Ticket::EventModulePost'} = {};
108
109    # extended input validation
110    my %SearchParams;
111
112    if ( $Self->GetOption('tickets-closed-before-date') ) {
113        %SearchParams = (
114            StateType                => 'Closed',
115            TicketCloseTimeOlderDate => $Self->GetOption('tickets-closed-before-date'),
116        );
117    }
118    elsif ( $Self->GetOption('tickets-closed-before-days') ) {
119        my $Seconds = $Self->GetOption('tickets-closed-before-days') * 60 * 60 * 24;
120
121        my $OlderDTObject = $Kernel::OM->Create('Kernel::System::DateTime');
122        $OlderDTObject->Subtract( Seconds => $Seconds );
123
124        %SearchParams = (
125            StateType                => 'Closed',
126            TicketCloseTimeOlderDate => $OlderDTObject->ToString(),
127        );
128    }
129
130    # If Archive system is enabled, take into account archived tickets as well.
131    # See bug#13945 (https://bugs.otrs.org/show_bug.cgi?id=13945).
132    if ( $Kernel::OM->Get('Kernel::Config')->{'Ticket::ArchiveSystem'} ) {
133        $SearchParams{ArchiveFlags} = [ 'y', 'n' ];
134    }
135
136    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
137
138    my @TicketIDs = $TicketObject->TicketSearch(
139        %SearchParams,
140        Result     => 'ARRAY',
141        OrderBy    => 'Up',
142        Limit      => 1_000_000_000,
143        UserID     => 1,
144        Permission => 'ro',
145    );
146
147    my $Count      = 0;
148    my $CountTotal = scalar @TicketIDs;
149
150    my $Target        = $Self->GetOption('target');
151    my %Target2Source = (
152        ArticleStorageFS => 'ArticleStorageDB',
153        ArticleStorageDB => 'ArticleStorageFS',
154    );
155
156    my $MicroSleep = $Self->GetOption('micro-sleep');
157    my $Tolerant   = $Self->GetOption('tolerant');
158
159    TICKETID:
160    for my $TicketID (@TicketIDs) {
161
162        $Count++;
163
164        $Self->Print("$Count/$CountTotal (TicketID:$TicketID)\n");
165
166        my $Success = $TicketObject->TicketArticleStorageSwitch(
167            TicketID    => $TicketID,
168            Source      => $Target2Source{$Target},
169            Destination => $Target,
170            UserID      => 1,
171        );
172
173        return $Self->ExitCodeError() if !$Tolerant && !$Success;
174
175        Time::HiRes::usleep($MicroSleep) if ($MicroSleep);
176    }
177
178    $Self->Print("<green>Done.</green>\n");
179
180    return $Self->ExitCodeOk();
181
182}
183
184sub PostRun {
185    my ($Self) = @_;
186
187    return $Kernel::OM->Get('Kernel::System::PID')->PIDDelete( Name => $Self->Name() );
188}
189
1901;
191